/*-------------------------------------------------------------------------
 *
 * nodeModifyTable.c
 *	  处理ModifyTable节点的例程。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/executor/nodeModifyTable.c
 *
 *-------------------------------------------------------------------------
 */
/* 接口例程
 *		ExecInitModifyTable - 初始化 ModifyTable 节点
 *		ExecModifyTable		- 从节点检索下一个元组
 *		ExecEndModifyTable	- 关闭 ModifyTable 节点
 *		ExecReScanModifyTable - 重新扫描 ModifyTable 节点
 *
 *	 注意
 *		ModifyTable 节点从其 outerPlan 接收输入，
 *		这是插入情况的插入数据，更新和合并情况的
 *		新值plus行定位信息或仅为删除情况的行定位信息。
 *
 *		要修改的关系可以是普通表，有 INSTEAD OF 触发器的视图，
 *		或外部表。早期处理已将 ModifyTable 指向
 *		不使用 INSTEAD OF 触发器的任何可自动更新视图的底层关系，
 *		因此此处的代码可以假设它不会作为修改目标。
 *		此节点确实处理 ri_WithCheckOptions，这可能包含来自
 *		那些可自动更新视图的表达式。
 *
 *		MERGE 在源关系和目标表之间运行连接；
 *		如果存在任何 WHEN NOT MATCHED 子句，则连接是外连接。
 *		在这种情况下，任何未匹配的元组将具有 NULL 行定位信息，
 *		并且只能运行插入。但对于匹配的元组，那么行
 *		定位信息将用于确定要更新或删除的元组。
 *		当所有子句都是 WHEN MATCHED 时，则使用内部连接，
 *		因此所有元组都包含行定位信息。
 *
 *		如果查询指定 RETURNING，则 ModifyTable 在完成
 *		每行插入、更新或删除后返回一个 RETURNING 元组。
 *		必须再次调用以继续操作。没有 RETURNING，
 *		我们只是在节点内循环，直到所有工作完成，然后
 *		返回 NULL。这避免了无用的调用/返回开销。
 *		（MERGE 不支持 RETURNING。）
 */

#include "postgres.h"

#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
#include "executor/nodeModifyTable.h"
#include "foreign/fdwapi.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/optimizer.h"
#include "rewrite/rewriteHandler.h"
#include "storage/bufmgr.h"
#include "storage/lmgr.h"
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/memutils.h"
#include "utils/rel.h"


typedef struct MTTargetRelLookup
{
	Oid			relationOid;	/* 哈希键，必须是第一个 */
	int			relationIndex;	/* rel 在 resultRelInfo[] 数组中的索引 */
} MTTargetRelLookup;

/*
 * ModifyTable 操作的上下文结构，包含基本执行状态
 * 和一些输出变量，这些变量由 ExecUpdateAct() 和
 * ExecDeleteAct() 填充，以向调用者报告其操作的结果。
 */
typedef struct ModifyTableContext
{
	/* 操作状态 */
	ModifyTableState *mtstate;
	EPQState   *epqstate;
	EState	   *estate;

	/*
	 * 存放从 ModifyTable 的子计划中获得的元组的槽。
	 * 用于访问不会存储的“垃圾”列。
	 */
	TupleTableSlot *planSlot;

	/* MERGE 特定 */
	MergeActionState *relaction;	/* MERGE 操作正在进行中 */

	/*
	 * 关于并发更改的元组的信息
	 * 正在更新或删除
	 */
	TM_FailureData tmfd;

	/*
	 * 在进行跨分区更新时，INSERT的RETURNING子句投影的元组
	 */
	TupleTableSlot *cpUpdateReturningSlot;
} ModifyTableContext;

/*
 * 包含特定于 UPDATE 操作的输出数据的上下文结构体。
 */
typedef struct UpdateContext
{
	bool		updated;		/* 更新实际上发生了吗？ */
	bool		updateIndexes;	/* 需要索引更新吗？ */
	bool		crossPartUpdate;	/* 这是跨分区更新吗？ */

	/*
	 * 锁定模式以获取在执行 EvalPlanQual 之前最新的元组版本
	 */
	LockTupleMode lockmode;
} UpdateContext;


static void fc_ExecBatchInsert(ModifyTableState *fc_mtstate,
							ResultRelInfo *fc_resultRelInfo,
							TupleTableSlot **fc_slots,
							TupleTableSlot **fc_planSlots,
							int fc_numSlots,
							EState *fc_estate,
							bool fc_canSetTag);
static void fc_ExecPendingInserts(EState *fc_estate);
static void fc_ExecCrossPartitionUpdateForeignKey(ModifyTableContext *fc_context,
											   ResultRelInfo *fc_sourcePartInfo,
											   ResultRelInfo *fc_destPartInfo,
											   ItemPointer fc_tupleid,
											   TupleTableSlot *fc_oldslot,
											   TupleTableSlot *fc_newslot);
static bool fc_ExecOnConflictUpdate(ModifyTableContext *fc_context,
								 ResultRelInfo *fc_resultRelInfo,
								 ItemPointer fc_conflictTid,
								 TupleTableSlot *fc_excludedSlot,
								 bool fc_canSetTag,
								 TupleTableSlot **fc_returning);
static TupleTableSlot *fc_ExecPrepareTupleRouting(ModifyTableState *fc_mtstate,
											   EState *fc_estate,
											   PartitionTupleRouting *fc_proute,
											   ResultRelInfo *fc_targetRelInfo,
											   TupleTableSlot *fc_slot,
											   ResultRelInfo **fc_partRelInfo);

static TupleTableSlot *fc_ExecMerge(ModifyTableContext *fc_context,
								 ResultRelInfo *fc_resultRelInfo,
								 ItemPointer fc_tupleid,
								 bool fc_canSetTag);
static void ExecInitMerge(ModifyTableState *mtstate, EState *estate);
static bool fc_ExecMergeMatched(ModifyTableContext *fc_context,
							 ResultRelInfo *fc_resultRelInfo,
							 ItemPointer fc_tupleid,
							 bool fc_canSetTag);
static void fc_ExecMergeNotMatched(ModifyTableContext *fc_context,
								ResultRelInfo *fc_resultRelInfo,
								bool fc_canSetTag);


/*
 * 验证 INSERT 产生的元组是否匹配目标关系的行类型
 *
 * 我们这样做是为了防止过时的计划。如果计划失效机制
 * 正常运作，那么我们在这里就不应该出现失败，但小心总比遗憾好。请注意，在我们获得目标关系的锁定后调用此函数，因此行类型不能在我们之下发生变化。
 *
 * 计划输出由其目标列表表示，因为这使得处理被删除列的情况更容易。
 *
 * 我们以前也将其用于 UPDATE，但现在等效的检查
 * 在 ExecBuildUpdateProjection 中完成。
 */
static void fc_ExecCheckPlanOutput(Relation fc_resultRel, List *fc_targetList)
{
	TupleDesc	fc_resultDesc = RelationGetDescr(fc_resultRel);
	int			fc_attno = 0;
	ListCell   *fc_lc;

	foreach(fc_lc, fc_targetList)
	{
		TargetEntry *fc_tle = (TargetEntry *) lfirst(fc_lc);
		Form_pg_attribute fc_attr;

		Assert(!fc_tle->resjunk);	/* 调用者已移除无用项 */

		if (fc_attno >= fc_resultDesc->natts)
			ereport(ERROR,
					(errcode(ERRCODE_DATATYPE_MISMATCH),
					 errmsg("table row type and query-specified row type do not match"),
					 errdetail("Query has too many columns.")));
		fc_attr = TupleDescAttr(fc_resultDesc, fc_attno);
		fc_attno++;

		if (!fc_attr->attisdropped)
		{
			/* 正常情况：要求类型匹配 */
			if (exprType((Node *) fc_tle->expr) != fc_attr->atttypid)
				ereport(ERROR,
						(errcode(ERRCODE_DATATYPE_MISMATCH),
						 errmsg("table row type and query-specified row type do not match"),
						 errdetail("Table has type %s at ordinal position %d, but query expects %s.",
								   format_type_be(fc_attr->atttypid),
								   fc_attno,
								   format_type_be(exprType((Node *) fc_tle->expr)))));
		}
		else
		{
			/*
			 * 对于被删除的列，我们无法检查 atttypid（它可能为 0）。
			 * 在任何情况下，计划程序很可能插入了一个 INT4 null。
			 * 我们坚持的只是 *一些* NULL 常量。
			 */
			if (!IsA(fc_tle->expr, Const) ||
				!((Const *) fc_tle->expr)->constisnull)
				ereport(ERROR,
						(errcode(ERRCODE_DATATYPE_MISMATCH),
						 errmsg("table row type and query-specified row type do not match"),
						 errdetail("Query provides a value for a dropped column at ordinal position %d.",
								   fc_attno)));
		}
	}
	if (fc_attno != fc_resultDesc->natts)
		ereport(ERROR,
				(errcode(ERRCODE_DATATYPE_MISMATCH),
				 errmsg("table row type and query-specified row type do not match"),
				 errdetail("Query has too few columns.")));
}

/*
 * ExecProcessReturning --- 评估 RETURNING 列表
 *
 * resultRelInfo：当前结果关系
 * tupleSlot：持有实际插入/更新/删除的元组的槽
 * planSlot：持有由顶层子计划节点返回的元组的槽
 *
 * 注意：如果 tupleSlot 为 NULL，FDW 应该已经提供了 econtext 的
 * 扫描元组。
 *
 * 返回一个持有结果元组的槽
 */
static TupleTableSlot * fc_ExecProcessReturning(ResultRelInfo *fc_resultRelInfo,
					 TupleTableSlot *fc_tupleSlot,
					 TupleTableSlot *fc_planSlot)
{
	ProjectionInfo *fc_projectReturning = fc_resultRelInfo->ri_projectReturning;
	ExprContext *fc_econtext = fc_projectReturning->pi_exprContext;

	/* 使元组和任何需要的连接变量可用于 ExecProject */
	if (fc_tupleSlot)
		fc_econtext->ecxt_scantuple = fc_tupleSlot;
	fc_econtext->ecxt_outertuple = fc_planSlot;

	/*
	 * RETURNING 表达式可能引用 tableoid 列，因此
	 * 在评估它们之前重新初始化 tts_tableOid。
	 */
	fc_econtext->ecxt_scantuple->tts_tableOid =
		RelationGetRelid(fc_resultRelInfo->ri_RelationDesc);

	/* 计算 RETURNING 表达式 */
	return ExecProject(fc_projectReturning);
}

/*
 * ExecCheckTupleVisible -- 验证元组是否可见
 *
 * 如果在非可见的 MVCC 快照基础上继续避免插入（采取投机插入的替代路径），
 * 将不符合更高隔离级别的保证。检查是否需要引发序列化失败，并在必要时执行。
 */
static void fc_ExecCheckTupleVisible(EState *fc_estate,
					  Relation fc_rel,
					  TupleTableSlot *fc_slot)
{
	if (!IsolationUsesXactSnapshot())
		return;

	if (!table_tuple_satisfies_snapshot(fc_rel, fc_slot, fc_estate->es_snapshot))
	{
		Datum		fc_xminDatum;
		TransactionId fc_xmin;
		bool		fc_isnull;

		fc_xminDatum = slot_getsysattr(fc_slot, MinTransactionIdAttributeNumber, &fc_isnull);
		Assert(!fc_isnull);
		fc_xmin = DatumGetTransactionId(fc_xminDatum);

		/*
		 * 如果冲突是针对我们自己事务插入的元组，即使它对我们的快照不可见，
		 * 我们也不应该引发序列化失败。（例如，如果在单个命令中
		 * 提出了冲突的键以进行插入，则会发生这种情况。）
		 */
		if (!TransactionIdIsCurrentTransactionId(fc_xmin))
			ereport(ERROR,
					(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
					 errmsg("could not serialize access due to concurrent update")));
	}
}

/*
 * ExecCheckTIDVisible -- ExecCheckTupleVisible() 的便捷变体
 */
static void fc_ExecCheckTIDVisible(EState *fc_estate,
					ResultRelInfo *fc_relinfo,
					ItemPointer fc_tid,
					TupleTableSlot *fc_tempSlot)
{
	Relation	fc_rel = fc_relinfo->ri_RelationDesc;

	/* 冗余检查隔离级别 */
	if (!IsolationUsesXactSnapshot())
		return;

	if (!table_tuple_fetch_row_version(fc_rel, fc_tid, SnapshotAny, fc_tempSlot))
		elog(ERROR, "failed to fetch conflicting tuple for ON CONFLICT");
	fc_ExecCheckTupleVisible(fc_estate, fc_rel, fc_tempSlot);
	ExecClearTuple(fc_tempSlot);
}

/*
 * 初始化以计算元组的存储生成列
 *
 * 这填充 resultRelInfo 的 ri_GeneratedExprs 字段，并创建一个关联的 ResultRelInfoExtra 结构以保持 ri_extraUpdatedCols。
 * （目前，ri_extraUpdatedCols 仅在 UPDATE 中进行查询，但我们必须在其他情况下填充它，例如 cmdtype 可能是 MERGE，但可能稍后会发生 UPDATE。）
 */
void ExecInitStoredGenerated(ResultRelInfo *fc_resultRelInfo,
						EState *fc_estate,
						CmdType fc_cmdtype)
{
	Relation	fc_rel = fc_resultRelInfo->ri_RelationDesc;
	TupleDesc	fc_tupdesc = RelationGetDescr(fc_rel);
	int			fc_natts = fc_tupdesc->natts;
	Bitmapset  *fc_updatedCols;
	ResultRelInfoExtra *fc_rextra;
	MemoryContext fc_oldContext;

	/* 不要重复调用 */
	Assert(fc_resultRelInfo->ri_GeneratedExprs == NULL);

	/* 如果没有生成列则无需处理 */
	if (!(fc_tupdesc->constr && fc_tupdesc->constr->has_generated_stored))
		return;

	/*
	 * 在 UPDATE 中，我们可以跳过计算任何不依赖于任何 UPDATE 目标列的生成列。
	 * 但如果有 BEFORE ROW UPDATE 触发器，我们不能跳过，因为触发器可能会更改更多列。
	 */
	if (fc_cmdtype == CMD_UPDATE &&
		!(fc_rel->trigdesc && fc_rel->trigdesc->trig_update_before_row))
		fc_updatedCols = ExecGetUpdatedCols(fc_resultRelInfo, fc_estate);
	else
		fc_updatedCols = NULL;

	/*
	 * 确保这些数据结构在每个查询的内存上下文中构建，
	 * 以便它们在整个查询期间生存。
	 */
	fc_oldContext = MemoryContextSwitchTo(fc_estate->es_query_cxt);

	fc_resultRelInfo->ri_GeneratedExprs =
		(ExprState **) palloc0(fc_natts * sizeof(ExprState *));
	fc_resultRelInfo->ri_NumGeneratedNeeded = 0;

	fc_rextra = palloc_object(ResultRelInfoExtra);
	fc_rextra->rinfo = fc_resultRelInfo;
	fc_rextra->ri_extraUpdatedCols = NULL;
	fc_estate->es_resultrelinfo_extra = lappend(fc_estate->es_resultrelinfo_extra,
											 fc_rextra);

	for (int fc_i = 0; fc_i < fc_natts; fc_i++)
	{
		if (TupleDescAttr(fc_tupdesc, fc_i)->attgenerated == ATTRIBUTE_GENERATED_STORED)
		{
			Expr	   *fc_expr;

			/* 获取 GENERATED AS 表达式树 */
			fc_expr = (Expr *) build_column_default(fc_rel, fc_i + 1);
			if (fc_expr == NULL)
				elog(ERROR, "no generation expression found for column number %d of table \"%s\"",
					 fc_i + 1, RelationGetRelationName(fc_rel));

			/*
			 * 如果这是一个已知的更新目标列的更新，请查看我们是否可以跳过计算。
			 */
			if (fc_updatedCols)
			{
				Bitmapset  *fc_attrs_used = NULL;

				pull_varattnos((Node *) fc_expr, 1, &fc_attrs_used);

				if (!bms_overlap(fc_updatedCols, fc_attrs_used))
					continue;	/* 不需要更新此列 */
			}

			/* 没有运气，因此为执行准备表达式 */
			fc_resultRelInfo->ri_GeneratedExprs[fc_i] = ExecPrepareExpr(fc_expr, fc_estate);
			fc_resultRelInfo->ri_NumGeneratedNeeded++;

			/* 并在 rextra->ri_extraUpdatedCols 中标记此列 */
			fc_rextra->ri_extraUpdatedCols =
				bms_add_member(fc_rextra->ri_extraUpdatedCols,
							   fc_i + 1 - FirstLowInvalidHeapAttributeNumber);
		}
	}

	MemoryContextSwitchTo(fc_oldContext);
}

/*
 * 计算元组的存储生成列
 */
void ExecComputeStoredGenerated(ResultRelInfo *fc_resultRelInfo,
						   EState *fc_estate, TupleTableSlot *fc_slot,
						   CmdType fc_cmdtype)
{
	Relation	fc_rel = fc_resultRelInfo->ri_RelationDesc;
	TupleDesc	fc_tupdesc = RelationGetDescr(fc_rel);
	int			fc_natts = fc_tupdesc->natts;
	ExprContext *fc_econtext = GetPerTupleExprContext(fc_estate);
	MemoryContext fc_oldContext;
	Datum	   *fc_values;
	bool	   *fc_nulls;

	/* 我们不应在此调用，除非这一点为真 */
	Assert(fc_tupdesc->constr && fc_tupdesc->constr->has_generated_stored);

	/*
	 * 对于在查询中直接命名的关系，ExecInitStoredGenerated
	 * 应该已经被调用；但对于分区子关系可能尚未发生。
	 * 此外，外部调用者不必显式调用 ExecInitStoredGenerated 也很方便。
	 */
	if (fc_resultRelInfo->ri_GeneratedExprs == NULL)
		ExecInitStoredGenerated(fc_resultRelInfo, fc_estate, fc_cmdtype);

	/*
	 * 如果该更改未影响任何生成列，则跳过其余部分。
	 */
	if (fc_resultRelInfo->ri_NumGeneratedNeeded == 0)
		return;

	fc_oldContext = MemoryContextSwitchTo(GetPerTupleMemoryContext(fc_estate));

	fc_values = palloc(sizeof(*fc_values) * fc_natts);
	fc_nulls = palloc(sizeof(*fc_nulls) * fc_natts);

	slot_getallattrs(fc_slot);
	memcpy(fc_nulls, fc_slot->tts_isnull, sizeof(*fc_nulls) * fc_natts);

	for (int fc_i = 0; fc_i < fc_natts; fc_i++)
	{
		Form_pg_attribute fc_attr = TupleDescAttr(fc_tupdesc, fc_i);

		if (fc_resultRelInfo->ri_GeneratedExprs[fc_i])
		{
			Datum		fc_val;
			bool		fc_isnull;

			Assert(fc_attr->attgenerated == ATTRIBUTE_GENERATED_STORED);

			fc_econtext->ecxt_scantuple = fc_slot;

			fc_val = ExecEvalExpr(fc_resultRelInfo->ri_GeneratedExprs[fc_i], fc_econtext, &fc_isnull);

			/*
			 * 我们必须复制 val，因为我们无法保证
			 * 引用传递 Datum 的内存位置。
			 */
			if (!fc_isnull)
				fc_val = datumCopy(fc_val, fc_attr->attbyval, fc_attr->attlen);

			fc_values[fc_i] = fc_val;
			fc_nulls[fc_i] = fc_isnull;
		}
		else
		{
			if (!fc_nulls[fc_i])
				fc_values[fc_i] = datumCopy(fc_slot->tts_values[fc_i], fc_attr->attbyval, fc_attr->attlen);
		}
	}

	ExecClearTuple(fc_slot);
	memcpy(fc_slot->tts_values, fc_values, sizeof(*fc_values) * fc_natts);
	memcpy(fc_slot->tts_isnull, fc_nulls, sizeof(*fc_nulls) * fc_natts);
	ExecStoreVirtualTuple(fc_slot);
	ExecMaterializeSlot(fc_slot);

	MemoryContextSwitchTo(fc_oldContext);
}

/*
 * ExecInitInsertProjection
 *		对 INSERT 元组的投影数据进行一次性初始化。
 *
 * INSERT 查询可能需要投影以过滤掉 tlist 中的无用属性。
 *
 * 这也是一个方便的地方，以验证 INSERT 的输出是否匹配目标表。
 */
static void fc_ExecInitInsertProjection(ModifyTableState *fc_mtstate,
						 ResultRelInfo *fc_resultRelInfo)
{
	ModifyTable *fc_node = (ModifyTable *) fc_mtstate->ps.plan;
	Plan	   *fc_subplan = outerPlan(fc_node);
	EState	   *fc_estate = fc_mtstate->ps.state;
	List	   *fc_insertTargetList = NIL;
	bool		fc_need_projection = false;
	ListCell   *fc_l;

	/* 提取子计划的结果 tlist 的非无用列。 */
	foreach(fc_l, fc_subplan->targetlist)
	{
		TargetEntry *fc_tle = (TargetEntry *) lfirst(fc_l);

		if (!fc_tle->resjunk)
			fc_insertTargetList = lappend(fc_insertTargetList, fc_tle);
		else
			fc_need_projection = true;
	}

	/*
	 * 没有垃圾的列表必须产生一个适合结果关系的元组。
	 */
	fc_ExecCheckPlanOutput(fc_resultRelInfo->ri_RelationDesc, fc_insertTargetList);

	/* 我们需要一个与表格式匹配的槽。 */
	fc_resultRelInfo->ri_newTupleSlot =
		table_slot_create(fc_resultRelInfo->ri_RelationDesc,
						  &fc_estate->es_tupleTable);

	/* 根据需要构建 ProjectionInfo（可能不需要）。 */
	if (fc_need_projection)
	{
		TupleDesc	fc_relDesc = RelationGetDescr(fc_resultRelInfo->ri_RelationDesc);

		/* 需要一个表达式上下文来进行投影 */
		if (fc_mtstate->ps.ps_ExprContext == NULL)
			ExecAssignExprContext(fc_estate, &fc_mtstate->ps);

		fc_resultRelInfo->ri_projectNew =
			ExecBuildProjectionInfo(fc_insertTargetList,
									fc_mtstate->ps.ps_ExprContext,
									fc_resultRelInfo->ri_newTupleSlot,
									&fc_mtstate->ps,
									fc_relDesc);
	}

	fc_resultRelInfo->ri_projectNewInfoValid = true;
}

/*
 * ExecInitUpdateProjection
 *		对 UPDATE 元组的一次性初始化投影数据。
 *
 * UPDATE 始终需要一个投影，因为（1）总会有一些垃圾属性， 
 * 和（2）我们可能需要将旧元组中未更新列的值合并到最终元组中。
 * 在 UPDATE 中，从子计划到达的元组仅包含已更改列的新值，以及垃圾属性中的行身份信息。
 *
 * 对于任何给定的结果关系，这是“一次性的”，
 * 但在继承的 UPDATE 过程中，我们可能会接触到多个结果关系，
 * 并且由于可能的列顺序变动，每个关系都需要自己的投影。
 *
 * 这里也是验证 UPDATE 输出是否与目标表匹配的方便之处 
 * （ExecBuildUpdateProjection 会这样做）。
 */
static void fc_ExecInitUpdateProjection(ModifyTableState *fc_mtstate,
						 ResultRelInfo *fc_resultRelInfo)
{
	ModifyTable *fc_node = (ModifyTable *) fc_mtstate->ps.plan;
	Plan	   *fc_subplan = outerPlan(fc_node);
	EState	   *fc_estate = fc_mtstate->ps.state;
	TupleDesc	fc_relDesc = RelationGetDescr(fc_resultRelInfo->ri_RelationDesc);
	int			fc_whichrel;
	List	   *fc_updateColnos;

	/*
	 * 通常，mt_lastResultIndex 匹配目标关系。
	 * 如果不匹配，我们可以通过整数除法获取索引。
	 */
	fc_whichrel = fc_mtstate->mt_lastResultIndex;
	if (fc_resultRelInfo != fc_mtstate->resultRelInfo + fc_whichrel)
	{
		fc_whichrel = fc_resultRelInfo - fc_mtstate->resultRelInfo;
		Assert(fc_whichrel >= 0 && fc_whichrel < fc_mtstate->mt_nrels);
	}

	fc_updateColnos = (List *) list_nth(fc_node->updateColnosLists, fc_whichrel);

	/*
	 * 对于 UPDATE，我们使用旧元组填补子计划生成的元组中缺失的值，
	 * 以获取新元组。我们需要两个槽，两个槽都符合表的期望格式。
	 */
	fc_resultRelInfo->ri_oldTupleSlot =
		table_slot_create(fc_resultRelInfo->ri_RelationDesc,
						  &fc_estate->es_tupleTable);
	fc_resultRelInfo->ri_newTupleSlot =
		table_slot_create(fc_resultRelInfo->ri_RelationDesc,
						  &fc_estate->es_tupleTable);

	/* 需要一个表达式上下文来进行投影 */
	if (fc_mtstate->ps.ps_ExprContext == NULL)
		ExecAssignExprContext(fc_estate, &fc_mtstate->ps);

	fc_resultRelInfo->ri_projectNew =
		ExecBuildUpdateProjection(fc_subplan->targetlist,
								  false,	/* 子计划执行了评估 */
								  fc_updateColnos,
								  fc_relDesc,
								  fc_mtstate->ps.ps_ExprContext,
								  fc_resultRelInfo->ri_newTupleSlot,
								  &fc_mtstate->ps);

	fc_resultRelInfo->ri_projectNewInfoValid = true;
}

/*
 * ExecGetInsertNewTuple
 *		这准备了一个“新”元组，准备插入到给定的结果关系中，
 *		通过移除计划输出元组的任何垃圾列
 *		并（如果需要）将元组强制转换为正确的元组格式。
 */
static TupleTableSlot * fc_ExecGetInsertNewTuple(ResultRelInfo *fc_relinfo,
					  TupleTableSlot *fc_planSlot)
{
	ProjectionInfo *fc_newProj = fc_relinfo->ri_projectNew;
	ExprContext *fc_econtext;

	/*
	 * 如果不需要进行投影，只需确保槽是目标关系的正确类型。
	 * 如果 planSlot 是正确的类型，我们可以直接使用它，
	 * 否则将数据复制到 ri_newTupleSlot 中。
	 */
	if (fc_newProj == NULL)
	{
		if (fc_relinfo->ri_newTupleSlot->tts_ops != fc_planSlot->tts_ops)
		{
			ExecCopySlot(fc_relinfo->ri_newTupleSlot, fc_planSlot);
			return fc_relinfo->ri_newTupleSlot;
		}
		else
			return fc_planSlot;
	}

	/*
	 * 否则进行投影；由于投影输出槽是 ri_newTupleSlot，
	 * 这也会修正任何槽类型的问题。
	 *
	 * 注意：目前，这是死代码，因为 INSERT 情况不接收
	 * 任何垃圾列，所以从未进行投影。
	 */
	fc_econtext = fc_newProj->pi_exprContext;
	fc_econtext->ecxt_outertuple = fc_planSlot;
	return ExecProject(fc_newProj);
}

/*
 * ExecGetUpdateNewTuple
 *		通过将 UPDATE 子计划的输出元组
 *		（其中包含已更改列的值）与从旧元组中取得的未更改列结合，
 *		来准备一个“新”元组。
 *
 * 子计划元组可能还包含垃圾列，这些垃圾列将被忽略。
 * 注意，投影还确保我们有一个正确类型的槽。
 */
TupleTableSlot * ExecGetUpdateNewTuple(ResultRelInfo *fc_relinfo,
					  TupleTableSlot *fc_planSlot,
					  TupleTableSlot *fc_oldSlot)
{
	ProjectionInfo *fc_newProj = fc_relinfo->ri_projectNew;
	ExprContext *fc_econtext;

	/* 使用一些额外的 Assert 来防止外部调用者 */
	Assert(fc_relinfo->ri_projectNewInfoValid);
	Assert(fc_planSlot != NULL && !TTS_EMPTY(fc_planSlot));
	Assert(fc_oldSlot != NULL && !TTS_EMPTY(fc_oldSlot));

	fc_econtext = fc_newProj->pi_exprContext;
	fc_econtext->ecxt_outertuple = fc_planSlot;
	fc_econtext->ecxt_scantuple = fc_oldSlot;
	return ExecProject(fc_newProj);
}

/* ----------------------------------------------------------------
 *		ExecInsert
 *
 *		对于 INSERT，我们必须将元组插入目标关系
 *		（或其某个分区），并在索引关系中插入适当的元组。
 *
 *		slot 包含要存储的新元组值。
 *
 *		如果有任何返回結果，则返回 RETURNING 结果，否则返回 NULL。
 *		*inserted_tuple 是有效插入的元组；
 *		*inserted_destrel 是插入的关系。
 *		这些仅在成功时设置。
 *
 *		这可能会改变当前活动的元组转换映射
 *		mtstate->mt_transition_capture，因此调用者必须小心保存
 *		先前值，以免失去追踪。
 * ----------------------------------------------------------------
 */
static TupleTableSlot * fc_ExecInsert(ModifyTableContext *fc_context,
		   ResultRelInfo *fc_resultRelInfo,
		   TupleTableSlot *fc_slot,
		   bool fc_canSetTag,
		   TupleTableSlot **fc_inserted_tuple,
		   ResultRelInfo **insert_destrel)
{
	ModifyTableState *fc_mtstate = fc_context->mtstate;
	EState	   *fc_estate = fc_context->estate;
	Relation	fc_resultRelationDesc;
	List	   *fc_recheckIndexes = NIL;
	TupleTableSlot *fc_planSlot = fc_context->planSlot;
	TupleTableSlot *fc_result = NULL;
	TransitionCaptureState *fc_ar_insert_trig_tcs;
	ModifyTable *fc_node = (ModifyTable *) fc_mtstate->ps.plan;
	OnConflictAction fc_onconflict = fc_node->onConflictAction;
	PartitionTupleRouting *fc_proute = fc_mtstate->mt_partition_tuple_routing;
	MemoryContext fc_oldContext;

	/*
	 * 如果输入结果关系是分区表，找到插入元组的叶子分区。
	 */
	if (fc_proute)
	{
		ResultRelInfo *fc_partRelInfo;

		fc_slot = fc_ExecPrepareTupleRouting(fc_mtstate, fc_estate, fc_proute,
									   fc_resultRelInfo, fc_slot,
									   &fc_partRelInfo);
		fc_resultRelInfo = fc_partRelInfo;
	}

	ExecMaterializeSlot(fc_slot);

	fc_resultRelationDesc = fc_resultRelInfo->ri_RelationDesc;

	/*
	 * 打开表的索引，如果我们还没有这样做，以便
	 * 为插入的元组添加新的索引条目。
	 */
	if (fc_resultRelationDesc->rd_rel->relhasindex &&
		fc_resultRelInfo->ri_IndexRelationDescs == NULL)
		ExecOpenIndices(fc_resultRelInfo, fc_onconflict != ONCONFLICT_NONE);

	/*
	 * 行插入触发器之前。
	 *
	 * 注意：我们在 INSERT ... ON CONFLICT 语句中的每次插入尝试中触发
	 * 行插入触发器。我们不能在触发这些触发器之前检查约束
	 * 违例，因为它们可能会改变要插入的值。此外，它们可以运行任意用户定义
	 * 的带有副作用的代码，而我们不能仅通过不插入元组来取消它们。
	 */
	if (fc_resultRelInfo->ri_TrigDesc &&
		fc_resultRelInfo->ri_TrigDesc->trig_insert_before_row)
	{
		/* 刷新任何待处理的插入，以便行对触发器可见 */
		if (fc_estate->es_insert_pending_result_relations != NIL)
			fc_ExecPendingInserts(fc_estate);

		if (!ExecBRInsertTriggers(fc_estate, fc_resultRelInfo, fc_slot))
			return NULL;		/* "不做任何操作" */
	}

	/* 代替行插入触发器 */
	if (fc_resultRelInfo->ri_TrigDesc &&
		fc_resultRelInfo->ri_TrigDesc->trig_insert_instead_row)
	{
		if (!ExecIRInsertTriggers(fc_estate, fc_resultRelInfo, fc_slot))
			return NULL;		/* "不做任何操作" */
	}
	else if (fc_resultRelInfo->ri_FdwRoutine)
	{
		/*
		 * 生成的表达式可能引用 tableoid 列，
		 * 因此在评估之前（重新）初始化 tts_tableOid。
		 */
		fc_slot->tts_tableOid = RelationGetRelid(fc_resultRelInfo->ri_RelationDesc);

		/*
		 * 计算存储生成的列
		 */
		if (fc_resultRelationDesc->rd_att->constr &&
			fc_resultRelationDesc->rd_att->constr->has_generated_stored)
			ExecComputeStoredGenerated(fc_resultRelInfo, fc_estate, fc_slot,
									   CMD_INSERT);

		/*
		 * 如果 FDW 支持批处理，并且请求了批处理，
		 * 则累积行并按批次插入它们。
		 * 否则使用逐行插入。
		 */
		if (fc_resultRelInfo->ri_BatchSize > 1)
		{
			bool		fc_flushed = false;

			/*
			 * 当我们达到所需的批次大小时，执行插入。
			 */
			if (fc_resultRelInfo->ri_NumSlots == fc_resultRelInfo->ri_BatchSize)
			{
				fc_ExecBatchInsert(fc_mtstate, fc_resultRelInfo,
								fc_resultRelInfo->ri_Slots,
								fc_resultRelInfo->ri_PlanSlots,
								fc_resultRelInfo->ri_NumSlots,
								fc_estate, fc_canSetTag);
				fc_flushed = true;
			}

			fc_oldContext = MemoryContextSwitchTo(fc_estate->es_query_cxt);

			if (fc_resultRelInfo->ri_Slots == NULL)
			{
				fc_resultRelInfo->ri_Slots = palloc(sizeof(TupleTableSlot *) *
												 fc_resultRelInfo->ri_BatchSize);
				fc_resultRelInfo->ri_PlanSlots = palloc(sizeof(TupleTableSlot *) *
													 fc_resultRelInfo->ri_BatchSize);
			}

			
/*
			 * 初始化批处理槽。我们不知道需要多少槽，因此我们随着批次的增长来初始化它们，并且在不同批次之间保持它们。为了缓解资源所有者处理具有多个引用的对象（例如许多槽都引用同一元组描述符）时的低效，我们为每个槽复制相应的元组描述符。
			 */
			if (fc_resultRelInfo->ri_NumSlots >= fc_resultRelInfo->ri_NumSlotsInitialized)
			{
				TupleDesc	fc_tdesc = CreateTupleDescCopy(fc_slot->tts_tupleDescriptor);
				TupleDesc	fc_plan_tdesc =
				CreateTupleDescCopy(fc_planSlot->tts_tupleDescriptor);

				fc_resultRelInfo->ri_Slots[fc_resultRelInfo->ri_NumSlots] =
					MakeSingleTupleTableSlot(fc_tdesc, fc_slot->tts_ops);

				fc_resultRelInfo->ri_PlanSlots[fc_resultRelInfo->ri_NumSlots] =
					MakeSingleTupleTableSlot(fc_plan_tdesc, fc_planSlot->tts_ops);

				/* 记住我们初始化了多少个批处理槽 */
				fc_resultRelInfo->ri_NumSlotsInitialized++;
			}

			ExecCopySlot(fc_resultRelInfo->ri_Slots[fc_resultRelInfo->ri_NumSlots],
						 fc_slot);

			ExecCopySlot(fc_resultRelInfo->ri_PlanSlots[fc_resultRelInfo->ri_NumSlots],
						 fc_planSlot);

			/*
			 * 如果这些是存储在缓冲区中的第一个元组，分别将目标关系和mtstate添加到
			 * es_insert_pending_result_relations和
			 * es_insert_pending_modifytables 列表，除了在
			 * 上面进行了刷新， 在这种情况下它们应该已经添加到列表中，因此无需再做此操作。
			 */
			if (fc_resultRelInfo->ri_NumSlots == 0 && !fc_flushed)
			{
				Assert(!list_member_ptr(fc_estate->es_insert_pending_result_relations,
										fc_resultRelInfo));
				fc_estate->es_insert_pending_result_relations =
					lappend(fc_estate->es_insert_pending_result_relations,
							fc_resultRelInfo);
				fc_estate->es_insert_pending_modifytables =
					lappend(fc_estate->es_insert_pending_modifytables, fc_mtstate);
			}
			Assert(list_member_ptr(fc_estate->es_insert_pending_result_relations,
								   fc_resultRelInfo));

			fc_resultRelInfo->ri_NumSlots++;

			MemoryContextSwitchTo(fc_oldContext);

			return NULL;
		}

		/*
		 * 插入外部表：让FDW来处理
		 */
		fc_slot = fc_resultRelInfo->ri_FdwRoutine->ExecForeignInsert(fc_estate,
															   fc_resultRelInfo,
															   fc_slot,
															   fc_planSlot);

		if (fc_slot == NULL)		/* "不做任何操作" */
			return NULL;

		/*
		 * AFTER ROW触发器或RETURNING表达式可能会引用tableoid列，因此在评估它们之前重新初始化tts_tableOid。
		 * （这覆盖了FDW替换了槽的情况。）
		 */
		fc_slot->tts_tableOid = RelationGetRelid(fc_resultRelInfo->ri_RelationDesc);
	}
	else
	{
		WCOKind		fc_wco_kind;

		/*
		 * 约束和GENERATED表达式可能会引用tableoid列，因此在评估它们之前重新初始化tts_tableOid。
		 */
		fc_slot->tts_tableOid = RelationGetRelid(fc_resultRelationDesc);

		/*
		 * 计算存储生成的列
		 */
		if (fc_resultRelationDesc->rd_att->constr &&
			fc_resultRelationDesc->rd_att->constr->has_generated_stored)
			ExecComputeStoredGenerated(fc_resultRelInfo, fc_estate, fc_slot,
									   CMD_INSERT);

		/*
		 * 检查任何RLS WITH CHECK 策略。
		 *
		 * 通常我们应该检查INSERT策略。但如果插入是由于将元组移动到新分区的分区键更新的结果，我们应该检查UPDATE策略，因为我们正在执行在目标表上定义的策略，而不是在子分区上定义的策略。
		 *
		 * 如果我们正在运行MERGE，我们会参考我们正在执行的操作以了解我们是在对分区表进行INSERT还是UPDATE。
		 */
		if (fc_mtstate->operation == CMD_UPDATE)
			fc_wco_kind = WCO_RLS_UPDATE_CHECK;
		else if (fc_mtstate->operation == CMD_MERGE)
			fc_wco_kind = (fc_context->relaction->mas_action->commandType == CMD_UPDATE) ?
				WCO_RLS_UPDATE_CHECK : WCO_RLS_INSERT_CHECK;
		else
			fc_wco_kind = WCO_RLS_INSERT_CHECK;

		/*
		 * ExecWithCheckOptions() 将跳过不是我们此时所寻找的类型的 WCO。
		 */
		if (fc_resultRelInfo->ri_WithCheckOptions != NIL)
			ExecWithCheckOptions(fc_wco_kind, fc_resultRelInfo, fc_slot, fc_estate);

		/*
		 * 检查元组的约束。
		 */
		if (fc_resultRelationDesc->rd_att->constr)
			ExecConstraints(fc_resultRelInfo, fc_slot, fc_estate);

		/*
		 * 如果有分区约束，还要检查元组是否符合分区约束；但如果我们是通过元组路由来到这里的，并且分区上没有定义 BR 触发器，则不需要检查。
		 */
		if (fc_resultRelationDesc->rd_rel->relispartition &&
			(fc_resultRelInfo->ri_RootResultRelInfo == NULL ||
			 (fc_resultRelInfo->ri_TrigDesc &&
			  fc_resultRelInfo->ri_TrigDesc->trig_insert_before_row)))
			ExecPartitionCheck(fc_resultRelInfo, fc_slot, fc_estate, true);

		if (fc_onconflict != ONCONFLICT_NONE && fc_resultRelInfo->ri_NumIndices > 0)
		{
			/* 执行一个投机插入。 */
			uint32		fc_specToken;
			ItemPointerData fc_conflictTid;
			bool		fc_specConflict;
			List	   *fc_arbiterIndexes;

			fc_arbiterIndexes = fc_resultRelInfo->ri_onConflictArbiterIndexes;

			/*
			 * 首先进行非结论性的冲突检查。
			 *
			 * 我们还没有持有任何锁，因此这并不能保证后续的插入不会冲突。 但如果你运行了很多会冲突的 INSERT ON CONFLICT 语句，这可以避免留下许多被取消的投机插入。
			 *
			 * 如果在下面找到冲突，无论是在预检查期间，还是在投机性插入后重新检查时，我们都会回到这里。 如果某个 bug 使这是一个无限循环，最好允许中断。
			 */
	vlock:
			CHECK_FOR_INTERRUPTS();
			fc_specConflict = false;
			if (!ExecCheckIndexConstraints(fc_resultRelInfo, fc_slot, fc_estate,
										   &fc_conflictTid, fc_arbiterIndexes))
			{
				/* 找到已提交的冲突元组 */
				if (fc_onconflict == ONCONFLICT_UPDATE)
				{
					/*
					 * 如果是 ON CONFLICT DO UPDATE，则执行 UPDATE 部分。 如果因为另一个并发的 UPDATE/DELETE 对冲突元组造成 UPDATE 失败，请做好重试准备。
					 */
					TupleTableSlot *fc_returning = NULL;

					if (fc_ExecOnConflictUpdate(fc_context, fc_resultRelInfo,
											 &fc_conflictTid, fc_slot, fc_canSetTag,
											 &fc_returning))
					{
						InstrCountTuples2(&fc_mtstate->ps, 1);
						return fc_returning;
					}
					else
						goto vlock;
				}
				else
				{
					/*
					 * 如果是 ON CONFLICT DO NOTHING，什么都不做。 但是，验证该元组在更高隔离级别下对执行器的 MVCC 快照是可见的。
					 *
					 * 使用 ExecGetReturningSlot() 来存储待重新检查的元组并不优雅，但是我们不能简单使用输入槽，因为其可能不是兼容类型。 由于在 DO NOTHING 的情况下没有 ExecGetReturningSlot() 的冲突用法......
					 */
					Assert(fc_onconflict == ONCONFLICT_NOTHING);
					fc_ExecCheckTIDVisible(fc_estate, fc_resultRelInfo, &fc_conflictTid,
										ExecGetReturningSlot(fc_estate, fc_resultRelInfo));
					InstrCountTuples2(&fc_mtstate->ps, 1);
					return NULL;
				}
			}

			/*
			 * 在我们开始真正的插入之前，获取我们的“投机插入锁”。 其他人可以使用该锁等待我们决定是否继续插入，而不是等待整个事务完成。
			 */
			fc_specToken = SpeculativeInsertionLockAcquire(GetCurrentTransactionId());

			/* 使用投机令牌插入元组 */
			table_tuple_insert_speculative(fc_resultRelationDesc, fc_slot,
										   fc_estate->es_output_cid,
										   0,
										   NULL,
										   fc_specToken);

			/* 为元组插入索引条目 */
			fc_recheckIndexes = ExecInsertIndexTuples(fc_resultRelInfo,
												   fc_slot, fc_estate, false, true,
												   &fc_specConflict,
												   fc_arbiterIndexes);

			/* 相应地调整元组的状态 */
			table_tuple_complete_speculative(fc_resultRelationDesc, fc_slot,
											 fc_specToken, !fc_specConflict);

			/*
			 * 唤醒任何等待我们决定的人。 他们将重新检查元组，看到它不再是投机的，并像处理常规插入的元组一样等待我们的 XID。 或者如果我们杀死了元组，他们会看到它已死亡，并像元组从未存在过一样继续执行。
			 */
			SpeculativeInsertionLockRelease(GetCurrentTransactionId());

			/*
			 * 如果有冲突，从头开始。 我们将再次进行预检查，这次将找到冲突的元组（除非它在我们到达那里之前中止）。
			 */
			if (fc_specConflict)
			{
				list_free(fc_recheckIndexes);
				goto vlock;
			}

			/* 由于没有插入冲突，我们完成了 */
		}
		else
		{
			/* 正常插入元组 */
			table_tuple_insert(fc_resultRelationDesc, fc_slot,
							   fc_estate->es_output_cid,
							   0, NULL);

			/* 为元组插入索引条目 */
			if (fc_resultRelInfo->ri_NumIndices > 0)
				fc_recheckIndexes = ExecInsertIndexTuples(fc_resultRelInfo,
													   fc_slot, fc_estate, false,
													   false, NULL, NIL);
		}
	}

	if (fc_canSetTag)
		(fc_estate->es_processed)++;

	/*
	 * 如果这次插入是分区键更新的结果，导致元组移动到新分区，则将这一行放入过渡 NEW TABLE（如果有的话）。 我们需要对 DELETE 和 INSERT 进行分开处理，因为它们发生在不同的表中。
	 */
	fc_ar_insert_trig_tcs = fc_mtstate->mt_transition_capture;
	if (fc_mtstate->operation == CMD_UPDATE && fc_mtstate->mt_transition_capture
		&& fc_mtstate->mt_transition_capture->tcs_update_new_table)
	{
		ExecARUpdateTriggers(fc_estate, fc_resultRelInfo,
							 NULL, NULL,
							 NULL,
							 NULL,
							 fc_slot,
							 NULL,
							 fc_mtstate->mt_transition_capture,
							 false);

		/*
		 * 我们已经捕捉了 NEW TABLE 行，因此请确保下面触发的任何 AR INSERT 触发器不会再次捕捉它。
		 */
		fc_ar_insert_trig_tcs = NULL;
	}

	/* AFTER ROW INSERT 触发器 */
	ExecARInsertTriggers(fc_estate, fc_resultRelInfo, fc_slot, fc_recheckIndexes,
						 fc_ar_insert_trig_tcs);

	list_free(fc_recheckIndexes);

	/*
	 * 检查来自父视图的任何 WITH CHECK OPTION 约束。 我们需要在根据 SQL 规范测试所有约束和唯一性违规后执行此操作，因此我们在实际将记录插入堆和所有索引后进行此操作。
	 *
	 * 如果发现违规，ExecWithCheckOptions 将 elog(ERROR)，因此如果它违反了 WITH CHECK OPTION，元组将再也无法被看到。
	 *
	 * ExecWithCheckOptions() 将跳过不是我们此时所寻找的类型的 WCO。
	 */
	if (fc_resultRelInfo->ri_WithCheckOptions != NIL)
		ExecWithCheckOptions(WCO_VIEW_CHECK, fc_resultRelInfo, fc_slot, fc_estate);

	/* 如果存在，处理 RETURNING */
	if (fc_resultRelInfo->ri_projectReturning)
		fc_result = fc_ExecProcessReturning(fc_resultRelInfo, fc_slot, fc_planSlot);

	if (fc_inserted_tuple)
		*fc_inserted_tuple = fc_slot;
	if (insert_destrel)
		*insert_destrel = fc_resultRelInfo;

	return fc_result;
}

/* ----------------------------------------------------------------
 *		ExecBatchInsert
 *
 *		以高效的方式插入多个元组。
 *		目前，这处理没有 RETURNING 子句的插入外部表。
 * ----------------------------------------------------------------
 */
static void fc_ExecBatchInsert(ModifyTableState *fc_mtstate,
				ResultRelInfo *fc_resultRelInfo,
				TupleTableSlot **fc_slots,
				TupleTableSlot **fc_planSlots,
				int fc_numSlots,
				EState *fc_estate,
				bool fc_canSetTag)
{
	int			fc_i;
	int			fc_numInserted = fc_numSlots;
	TupleTableSlot *fc_slot = NULL;
	TupleTableSlot **fc_rslots;

	/*
	 * 插入外部表：让 FDW 来处理
	 */
	fc_rslots = fc_resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert(fc_estate,
																  fc_resultRelInfo,
																  fc_slots,
																  fc_planSlots,
																  &fc_numInserted);

	for (fc_i = 0; fc_i < fc_numInserted; fc_i++)
	{
		fc_slot = fc_rslots[fc_i];

		/*
		 * AFTER ROW 触发器可能会引用 tableoid 列，因此在评估它们之前，请（重新）初始化 tts_tableOid。
		 */
		fc_slot->tts_tableOid = RelationGetRelid(fc_resultRelInfo->ri_RelationDesc);

		/* AFTER ROW INSERT 触发器 */
		ExecARInsertTriggers(fc_estate, fc_resultRelInfo, fc_slot, NIL,
							 fc_mtstate->mt_transition_capture);

		/*
		 * 检查来自父视图的任何 WITH CHECK OPTION 约束。 请参见 ExecInsert 中的注释。
		 */
		if (fc_resultRelInfo->ri_WithCheckOptions != NIL)
			ExecWithCheckOptions(WCO_VIEW_CHECK, fc_resultRelInfo, fc_slot, fc_estate);
	}

	if (fc_canSetTag && fc_numInserted > 0)
		fc_estate->es_processed += fc_numInserted;

	/* 清理所有插槽，准备下一个批次 */
	for (fc_i = 0; fc_i < fc_numSlots; fc_i++)
	{
		ExecClearTuple(fc_slots[fc_i]);
		ExecClearTuple(fc_planSlots[fc_i]);
	}
	fc_resultRelInfo->ri_NumSlots = 0;
}

/*
 * ExecPendingInserts -- 将所有挂起的插入刷新到外部表
 */
static void fc_ExecPendingInserts(EState *fc_estate)
{
	ListCell   *fc_l1,
			   *fc_l2;

	forboth(fc_l1, fc_estate->es_insert_pending_result_relations,
			fc_l2, fc_estate->es_insert_pending_modifytables)
	{
		ResultRelInfo *fc_resultRelInfo = (ResultRelInfo *) lfirst(fc_l1);
		ModifyTableState *fc_mtstate = (ModifyTableState *) lfirst(fc_l2);

		Assert(fc_mtstate);
		fc_ExecBatchInsert(fc_mtstate, fc_resultRelInfo,
						fc_resultRelInfo->ri_Slots,
						fc_resultRelInfo->ri_PlanSlots,
						fc_resultRelInfo->ri_NumSlots,
						fc_estate, fc_mtstate->canSetTag);
	}

	list_free(fc_estate->es_insert_pending_result_relations);
	list_free(fc_estate->es_insert_pending_modifytables);
	fc_estate->es_insert_pending_result_relations = NIL;
	fc_estate->es_insert_pending_modifytables = NIL;
}

/*
 * ExecDeletePrologue -- ExecDelete 的子例程
 *
 * 为 DELETE 准备执行器状态。 事实上，这里我们唯一需要做的就是执行 BEFORE ROW 触发器。 如果其中一个使删除变成无操作，则返回 false；否则，返回 true。
 */
static bool fc_ExecDeletePrologue(ModifyTableContext *fc_context, ResultRelInfo *fc_resultRelInfo,
				   ItemPointer fc_tupleid, HeapTuple fc_oldtuple,
				   TupleTableSlot **fc_epqreturnslot, TM_Result *fc_result)
{
	if (fc_result)
		*fc_result = TM_Ok;

	/* BEFORE ROW DELETE 触发器 */
	if (fc_resultRelInfo->ri_TrigDesc &&
		fc_resultRelInfo->ri_TrigDesc->trig_delete_before_row)
	{
		/* 刷新任何待处理的插入，以便行对触发器可见 */
		if (fc_context->estate->es_insert_pending_result_relations != NIL)
			fc_ExecPendingInserts(fc_context->estate);

		return ExecBRDeleteTriggersNew(fc_context->estate, fc_context->epqstate,
									   fc_resultRelInfo, fc_tupleid, fc_oldtuple,
									   fc_epqreturnslot, fc_result, &fc_context->tmfd);
	}

	return true;
}


/*
 * ExecDeleteAct -- ExecDelete的子例程
 *
 * 实际上从普通表中删除元组。
 *
 * 调用者负责根据需要进行EvalPlanQual
 */
static TM_Result fc_ExecDeleteAct(ModifyTableContext *fc_context, ResultRelInfo *fc_resultRelInfo,
			  ItemPointer fc_tupleid, bool fc_changingPart)
{
	EState	   *fc_estate = fc_context->estate;

	return table_tuple_delete(fc_resultRelInfo->ri_RelationDesc, fc_tupleid,
							  fc_estate->es_output_cid,
							  fc_estate->es_snapshot,
							  fc_estate->es_crosscheck_snapshot,
							  true /* 等待提交 */ ,
							  &fc_context->tmfd,
							  fc_changingPart);
}

/*
 * ExecDeleteEpilogue -- ExecDelete的子例程
 *
 * 元组删除的结束步骤；这会调用每行触发器的AFTER， 
 * 包括如果删除是作为跨分区元组移动的一部分进行的情况下的UPDATE触发器。
 */
static void fc_ExecDeleteEpilogue(ModifyTableContext *fc_context, ResultRelInfo *fc_resultRelInfo,
				   ItemPointer fc_tupleid, HeapTuple fc_oldtuple, bool fc_changingPart)
{
	ModifyTableState *fc_mtstate = fc_context->mtstate;
	EState	   *fc_estate = fc_context->estate;
	TransitionCaptureState *fc_ar_delete_trig_tcs;

	/*
	 * 如果这个删除是分区键更新的结果，导致元组移动到新的分区， 
	 * 如果有过渡旧表，将这行放入其中。我们需要为DELETE和INSERT分别 
	 * 处理，因为它们发生在不同的表上。
	 */
	fc_ar_delete_trig_tcs = fc_mtstate->mt_transition_capture;
	if (fc_mtstate->operation == CMD_UPDATE && fc_mtstate->mt_transition_capture &&
		fc_mtstate->mt_transition_capture->tcs_update_old_table)
	{
		ExecARUpdateTriggers(fc_estate, fc_resultRelInfo,
							 NULL, NULL,
							 fc_tupleid, fc_oldtuple,
							 NULL, NULL, fc_mtstate->mt_transition_capture,
							 false);

		/*
		 * 我们已经捕获了 OLD TABLE 行，所以确保下面触发的任何 AR
		 * DELETE 不会再次捕获它。
		 */
		fc_ar_delete_trig_tcs = NULL;
	}

	/* 行删除之后触发器 */
	ExecARDeleteTriggers(fc_estate, fc_resultRelInfo, fc_tupleid, fc_oldtuple,
						 fc_ar_delete_trig_tcs, fc_changingPart);
}

/* ----------------------------------------------------------------
 *		ExecDelete
 *
 *		DELETE 类似于 UPDATE，不同之处在于我们删除元组，不需要
 *		进行索引修改。
 *
 *		从表中删除时，tupleid 标识要删除的元组，而 oldtuple 为 NULL。
 *		通过视图执行 INSTEAD OF 触发器时，oldtuple 被传递给
 *		触发器，并标识要删除的内容，tupleid 无效。 从外部表中
 *		删除时，tupleid 无效；FDW 必须使用计划插槽中的数据来
 *		确定要删除的行。 oldtuple 被传递给外部表触发器；当
 *		外部表没有相关触发器时，它为 NULL。 我们使用
 *		tupleDeleted 指示元组是否实际被删除，调用者可以使用
 *		它决定是否继续操作。 当此 DELETE 是一个更新的
 *		partition-key 的一部分时，通过输出参数 epqreturnslot
 *		返回 EvalPlanQual() 所返回的槽。
 *
 *		返回任何 RETURNING 结果，否则为 NULL。
 * ----------------------------------------------------------------
 */
static TupleTableSlot * fc_ExecDelete(ModifyTableContext *fc_context,
		   ResultRelInfo *fc_resultRelInfo,
		   ItemPointer fc_tupleid,
		   HeapTuple fc_oldtuple,
		   bool fc_processReturning,
		   bool fc_changingPart,
		   bool fc_canSetTag,
		   TM_Result *fc_tmresult,
		   bool *fc_tupleDeleted,
		   TupleTableSlot **fc_epqreturnslot)
{
	EState	   *fc_estate = fc_context->estate;
	Relation	fc_resultRelationDesc = fc_resultRelInfo->ri_RelationDesc;
	TupleTableSlot *fc_slot = NULL;
	TM_Result	fc_result;

	if (fc_tupleDeleted)
		*fc_tupleDeleted = false;

	/*
	 * 为删除准备。这包括 BEFORE ROW 触发器，因此如果它说我们
	 * 完成了，则我们也完成了。
	 */
	if (!fc_ExecDeletePrologue(fc_context, fc_resultRelInfo, fc_tupleid, fc_oldtuple,
							fc_epqreturnslot, fc_tmresult))
		return NULL;

	/* INSTEAD OF ROW DELETE 触发器 */
	if (fc_resultRelInfo->ri_TrigDesc &&
		fc_resultRelInfo->ri_TrigDesc->trig_delete_instead_row)
	{
		bool		fc_dodelete;

		Assert(fc_oldtuple != NULL);
		fc_dodelete = ExecIRDeleteTriggers(fc_estate, fc_resultRelInfo, fc_oldtuple);

		if (!fc_dodelete)			/* "不做任何操作" */
			return NULL;
	}
	else if (fc_resultRelInfo->ri_FdwRoutine)
	{
		/*
		 * 从外部表中删除：让 FDW 处理
		 *
		 * 我们提供返回插槽作为存储 RETURNING 数据的地方，
		 * 尽管 FDW 可以返回其他插槽（如果它愿意）。
		 */
		fc_slot = ExecGetReturningSlot(fc_estate, fc_resultRelInfo);
		fc_slot = fc_resultRelInfo->ri_FdwRoutine->ExecForeignDelete(fc_estate,
															   fc_resultRelInfo,
															   fc_slot,
															   fc_context->planSlot);

		if (fc_slot == NULL)		/* "不做任何操作" */
			return NULL;

		/*
		 * RETURNING 表达式可能引用 tableoid 列，因此在评估它们之前，
		 * 请（重新）初始化 tts_tableOid。
		 */
		if (TTS_EMPTY(fc_slot))
			ExecStoreAllNullTuple(fc_slot);

		fc_slot->tts_tableOid = RelationGetRelid(fc_resultRelationDesc);
	}
	else
	{
		/*
		 * 删除元组
		 *
		 * 注意：如果 context->estate->es_crosscheck_snapshot 不是
		 * InvalidSnapshot，我们将检查要删除的行是否对该快照可见，
		 * 如果不可见则抛出不能序列化的错误。这是事务快照模式
		 * 事务中对引用完整性更新所需的特例行为。
		 */
ldelete:;
		fc_result = fc_ExecDeleteAct(fc_context, fc_resultRelInfo, fc_tupleid, fc_changingPart);

		if (fc_tmresult)
			*fc_tmresult = fc_result;

		switch (fc_result)
		{
			case TM_SelfModified:

				/*
				 * 目标元组已被当前命令或当前事务中的后续命令更新或
				 * 删除。 前一种情况可能出现在连接 DELETE 中，其中多个
				 * 元组连接到同一目标元组。这有点可疑，但 Postgres 一直允许
				 * 这样做：我们只是忽略额外的删除尝试。
				 *
				 * 后一种情况发生在元组被 BEFORE 触发器中的命令修改，或者
				 * 在查询中使用的易变函数中的命令。在这种情况下，我们
				 * 不应忽略删除，但继续也同样不安全。我们不想在
				 * 保持基于其删除的触发动作的同时丢弃原始 DELETE；
				 * 允许原始 DELETE 而丢弃它触发的更新也没有更好。行
				 * 更新携带一些可能根据业务规则重要的信息；因此，抛出
				 * 错误是唯一安全的方式。
				 *
				 * 如果触发器确实打算这种类型的交互，它可以重新执行
				 * DELETE，然后返回 NULL 以取消外部删除。
				 */
				if (fc_context->tmfd.cmax != fc_estate->es_output_cid)
					ereport(ERROR,
							(errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
							 errmsg("tuple to be deleted was already modified by an operation triggered by the current command"),
							 errhint("Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.")));

				/* 否则，自身已经删除；无事可做 */
				return NULL;

			case TM_Ok:
				break;

			case TM_Updated:
				{
					TupleTableSlot *fc_inputslot;
					TupleTableSlot *fc_epqslot;

					if (IsolationUsesXactSnapshot())
						ereport(ERROR,
								(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
								 errmsg("could not serialize access due to concurrent update")));

					/*
					 * 已经知道我们需要执行 EPQ，因此直接将元组抓取到
					 * 正确的槽中。
					 */
					EvalPlanQualBegin(fc_context->epqstate);
					fc_inputslot = EvalPlanQualSlot(fc_context->epqstate, fc_resultRelationDesc,
												 fc_resultRelInfo->ri_RangeTableIndex);

					fc_result = table_tuple_lock(fc_resultRelationDesc, fc_tupleid,
											  fc_estate->es_snapshot,
											  fc_inputslot, fc_estate->es_output_cid,
											  LockTupleExclusive, LockWaitBlock,
											  TUPLE_LOCK_FLAG_FIND_LAST_VERSION,
											  &fc_context->tmfd);

					switch (fc_result)
					{
						case TM_Ok:
							Assert(fc_context->tmfd.traversed);
							fc_epqslot = EvalPlanQual(fc_context->epqstate,
												   fc_resultRelationDesc,
												   fc_resultRelInfo->ri_RangeTableIndex,
												   fc_inputslot);
							if (TupIsNull(fc_epqslot))
								/* 元组不再通过资格，正在退出... */
								return NULL;

							/*
							 * 如果请求，跳过删除并返回更新后的行。
							 */
							if (fc_epqreturnslot)
							{
								*fc_epqreturnslot = fc_epqslot;
								return NULL;
							}
							else
								goto ldelete;

						case TM_SelfModified:

							/*
							 * 当跟随由另一个会话更新的元组的更新链时，
							 * 可以到达此处，达到了在此事务中已更新的
							 * 元组。如果之前由此命令更新，则忽略删除，
							 * 否则报错。
							 *
							 * 另见上面 table_tuple_delete() 的 TM_SelfModified
							 * 响应。
							 */
							if (fc_context->tmfd.cmax != fc_estate->es_output_cid)
								ereport(ERROR,
										(errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
										 errmsg("tuple to be deleted was already modified by an operation triggered by the current command"),
										 errhint("Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.")));
							return NULL;

						case TM_Deleted:
							/* 元组已经被删除；无事可做 */
							return NULL;

						default:

							/*
							 * TM_Invisible 应该是不可能的，因为我们正在
							 * 等待更新的行版本，如果第一个版本不可见,
							 * 将已经报错。
							 *
							 * TM_Updated 应该是不可能的，因为我们通过
							 * TUPLE_LOCK_FLAG_FIND_LAST_VERSION 锁定最新
							 * 版本。
							 */
							elog(ERROR, "unexpected table_tuple_lock status: %u",
								 fc_result);
							return NULL;
					}

					Assert(false);
					break;
				}

			case TM_Deleted:
				if (IsolationUsesXactSnapshot())
					ereport(ERROR,
							(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
							 errmsg("could not serialize access due to concurrent delete")));
				/* 元组已经被删除；无事可做 */
				return NULL;

			default:
				elog(ERROR, "unrecognized table_tuple_delete status: %u",
					 fc_result);
				return NULL;
		}

		/*
		 * 注意：通常人们会认为我们现在必须删除与堆元组关联的
		 * 索引元组...
		 *
		 * ...但在 POSTGRES 中，我们不需要这样做，因为 VACUUM
		 * 会在之后处理它。我们无论如何都不能立即删除索引元组，
		 * 因为元组仍然对其他事务可见。
		 */
	}

	if (fc_canSetTag)
		(fc_estate->es_processed)++;

	/* 告诉调用者删除实际发生了。 */
	if (fc_tupleDeleted)
		*fc_tupleDeleted = true;

	fc_ExecDeleteEpilogue(fc_context, fc_resultRelInfo, fc_tupleid, fc_oldtuple, fc_changingPart);

	
/* 处理 RETURNING（如果存在并且被请求） */
	if (fc_processReturning && fc_resultRelInfo->ri_projectReturning)
	{
		/*
		 * 我们必须将目标元组放入一个槽中，这意味着我们首先
		 * 必须获取它。我们可以使用触发器元组槽。 
		 */
		TupleTableSlot *fc_rslot;

		if (fc_resultRelInfo->ri_FdwRoutine)
		{
			/* FDW 必须提供一个包含已删除行的槽 */
			Assert(!TupIsNull(fc_slot));
		}
		else
		{
			fc_slot = ExecGetReturningSlot(fc_estate, fc_resultRelInfo);
			if (fc_oldtuple != NULL)
			{
				ExecForceStoreHeapTuple(fc_oldtuple, fc_slot, false);
			}
			else
			{
				if (!table_tuple_fetch_row_version(fc_resultRelationDesc, fc_tupleid,
												   SnapshotAny, fc_slot))
					elog(ERROR, "failed to fetch deleted tuple for DELETE RETURNING");
			}
		}

		fc_rslot = fc_ExecProcessReturning(fc_resultRelInfo, fc_slot, fc_context->planSlot);

		/*
		 * 在再次释放目标元组之前，确保 rslot 拥有任何按引用传递值的
		 * 本地副本。 
		 */
		ExecMaterializeSlot(fc_rslot);

		ExecClearTuple(fc_slot);

		return fc_rslot;
	}

	return NULL;
}

/*
 * ExecCrossPartitionUpdate --- 将更新后的元组移动到另一个分区。
 *
 * 这通过首先从当前分区删除旧元组，然后将新元组插入到根父表中，即，
 * mtstate->rootResultRelInfo。它将从那里重新路由到
 * 正确的分区。
 *
 * 如果元组已成功移动，或者如果发现元组已被并发删除，因此调用者无需做更多
 * 工作，则返回 true。
 *
 * 如果我们尝试移动的元组发现已被并发更新，则返回 false。
 * 在这种情况下，调用者必须检查 *retry_slot 返回的更新元组是否仍然需要
 * 重新路由，并再次调用此函数或相应地执行常规更新。对于 MERGE，
 * 更新元组不会在 *retry_slot 中返回；它有自己的重试逻辑。
 */
static bool fc_ExecCrossPartitionUpdate(ModifyTableContext *fc_context,
						 ResultRelInfo *fc_resultRelInfo,
						 ItemPointer fc_tupleid, HeapTuple fc_oldtuple,
						 TupleTableSlot *fc_slot,
						 bool fc_canSetTag,
						 UpdateContext *fc_updateCxt,
						 TM_Result *fc_tmresult,
						 TupleTableSlot **fc_retry_slot,
						 TupleTableSlot **fc_inserted_tuple,
						 ResultRelInfo **insert_destrel)
{
	ModifyTableState *fc_mtstate = fc_context->mtstate;
	EState	   *fc_estate = fc_mtstate->ps.state;
	TupleConversionMap *fc_tupconv_map;
	bool		fc_tuple_deleted;
	TupleTableSlot *fc_epqslot = NULL;

	fc_context->cpUpdateReturningSlot = NULL;
	*fc_retry_slot = NULL;

	/*
	 * 不允许执行 INSERT ON CONFLICT DO UPDATE 的操作，这将导致原始行
	 * 迁移到不同的分区。也许这可以在某一天实现，但这似乎是一个边缘特性，
	 * 价值不大。
	 */
	if (((ModifyTable *) fc_mtstate->ps.plan)->onConflictAction == ONCONFLICT_UPDATE)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("invalid ON UPDATE specification"),
				 errdetail("The result tuple would appear in a different partition than the original tuple.")));

	/*
	 * 当 UPDATE 直接在叶子分区上运行时，简单地因
	 * 分区约束违反错误而失败。 
	 */
	if (fc_resultRelInfo == fc_mtstate->rootResultRelInfo)
		ExecPartitionCheckEmitError(fc_resultRelInfo, fc_slot, fc_estate);

	/* 如果尚未完成，则初始化元组路由信息。 */
	if (fc_mtstate->mt_partition_tuple_routing == NULL)
	{
		Relation	fc_rootRel = fc_mtstate->rootResultRelInfo->ri_RelationDesc;
		MemoryContext fc_oldcxt;

		/* 在这里构建的内容必须在查询持续期间有效。 */
		fc_oldcxt = MemoryContextSwitchTo(fc_estate->es_query_cxt);

		fc_mtstate->mt_partition_tuple_routing =
			ExecSetupPartitionTupleRouting(fc_estate, fc_rootRel);

		/*
		 * 在重新路由一个分区的元组之前，它必须首先转换为根的格式，
		 * 因此我们需要一个槽来存储这些元组。
		 */
		Assert(fc_mtstate->mt_root_tuple_slot == NULL);
		fc_mtstate->mt_root_tuple_slot = table_slot_create(fc_rootRel, NULL);

		MemoryContextSwitchTo(fc_oldcxt);
	}

	/*
	 * 行移动，第 1 部分。删除元组，但跳过 RETURNING 处理。
	 * 我们想要从 INSERT 中返回行。 
	 */
	fc_ExecDelete(fc_context, fc_resultRelInfo,
			   fc_tupleid, fc_oldtuple,
			   false,			/* processReturning */
			   true,			/* changingPart */
			   false,			/* canSetTag */
			   fc_tmresult, &fc_tuple_deleted, &fc_epqslot);

	/*
	 * 出于某种原因，如果删除操作没有发生（例如触发器阻止了它，或者
	 * 它已经被自身删除，或者它被另一个事务并发删除），那么我们也应该跳过插入；
	 * 否则，更新操作可能会导致所有分区中总行数增加，这显然是不正确的。
	 *
	 * 对于正常的更新操作，当元组被并发更新或删除时，会由 EvalPlanQual
	 * 机制处理，但对于我们已将其转换为从此分区删除并在其他分区插入的更新，
	 * 这并不适用，因为 CTID 链不能跨关系边界。我们通过跳过插入来模拟此语义，
	 * 如果删除操作未能找到元组。这确保了两个并发尝试同时更新相同元组时
	 * 不会将一个元组变成两个，并且刚刚删除的元组的更新无法复活它。
	 */
	if (!fc_tuple_deleted)
	{
		/*
		 * epqslot 通常会是 NULL。但当 ExecDelete() 发现
		 * 另一个事务并发更新了相同的行时，它会重新获取该行，跳过删除，
		 * epqslot 被设置为重新获取的元组槽。在这种情况下，我们需要再次进行
		 * 所有检查。对于 MERGE，我们将一切留给调用者（它必须进行额外的重新检查，
		 * 并可能最终执行完全不同的操作）。
		 */
		if (fc_context->relaction != NULL)
			return *fc_tmresult == TM_Ok;
		else if (TupIsNull(fc_epqslot))
			return true;
		else
		{
			/* 获取旧元组的最新版本。 */
			TupleTableSlot *fc_oldSlot;

			/* ...但首先，确保 ri_oldTupleSlot 已初始化。 */
			if (unlikely(!fc_resultRelInfo->ri_projectNewInfoValid))
				fc_ExecInitUpdateProjection(fc_mtstate, fc_resultRelInfo);
			fc_oldSlot = fc_resultRelInfo->ri_oldTupleSlot;
			if (!table_tuple_fetch_row_version(fc_resultRelInfo->ri_RelationDesc,
											   fc_tupleid,
											   SnapshotAny,
											   fc_oldSlot))
				elog(ERROR, "failed to fetch tuple being updated");
			/* 并投影新的元组以重试更新。 */
			*fc_retry_slot = ExecGetUpdateNewTuple(fc_resultRelInfo, fc_epqslot,
												fc_oldSlot);
			return false;
		}
	}

	/*
	 * resultRelInfo 是每个关系的 per-relation resultRelInfos 之一。
	 * 所以如果需要，我们应该将元组转换为根的元组描述符，因为
	 * ExecInsert() 从根开始查找。
	 */
	fc_tupconv_map = ExecGetChildToRootMap(fc_resultRelInfo);
	if (fc_tupconv_map != NULL)
		fc_slot = execute_attr_map_slot(fc_tupconv_map->attrMap,
									 fc_slot,
									 fc_mtstate->mt_root_tuple_slot);

	/* 元组路由从根表开始。 */
	fc_context->cpUpdateReturningSlot =
		fc_ExecInsert(fc_context, fc_mtstate->rootResultRelInfo, fc_slot, fc_canSetTag,
				   fc_inserted_tuple, insert_destrel);

	/*
	 * 重置可能已被 INSERT 写入的过渡状态。
	 */
	if (fc_mtstate->mt_transition_capture)
		fc_mtstate->mt_transition_capture->tcs_original_insert_tuple = NULL;

	/* 我们完成了移动。 */
	return true;
}

/*
 * ExecUpdatePrologue -- ExecUpdate 的子例程
 *
 * 为更新准备执行器状态。这包括运行行前触发器。
 * 如果其中一个触发器使更新变为无效，则返回 false；否则，返回 true。
 */
static bool fc_ExecUpdatePrologue(ModifyTableContext *fc_context, ResultRelInfo *fc_resultRelInfo,
				   ItemPointer fc_tupleid, HeapTuple fc_oldtuple, TupleTableSlot *fc_slot,
				   TM_Result *fc_result)
{
	Relation	fc_resultRelationDesc = fc_resultRelInfo->ri_RelationDesc;

	if (fc_result)
		*fc_result = TM_Ok;

	ExecMaterializeSlot(fc_slot);

	/*
	 * 打开表的索引，如果我们还没有这么做，以便可以为更新的元组
	 * 添加新的索引条目。
	 */
	if (fc_resultRelationDesc->rd_rel->relhasindex &&
		fc_resultRelInfo->ri_IndexRelationDescs == NULL)
		ExecOpenIndices(fc_resultRelInfo, false);

	/* 行前更新触发器 */
	if (fc_resultRelInfo->ri_TrigDesc &&
		fc_resultRelInfo->ri_TrigDesc->trig_update_before_row)
	{
		/* 刷新任何待处理的插入，以便行对触发器可见 */
		if (fc_context->estate->es_insert_pending_result_relations != NIL)
			fc_ExecPendingInserts(fc_context->estate);

		return ExecBRUpdateTriggersNew(fc_context->estate, fc_context->epqstate,
									   fc_resultRelInfo, fc_tupleid, fc_oldtuple, fc_slot,
									   fc_result, &fc_context->tmfd);
	}

	return true;
}

/*
 * ExecUpdatePrepareSlot -- ExecUpdateAct 的子例程
 *
 * 在更新之前对元组槽进行最终修改。
 * （这是分开的，因为我们在外部表代码路径中也需要它。）
 */
static void fc_ExecUpdatePrepareSlot(ResultRelInfo *fc_resultRelInfo,
					  TupleTableSlot *fc_slot,
					  EState *fc_estate)
{
	Relation	fc_resultRelationDesc = fc_resultRelInfo->ri_RelationDesc;

	/*
	 * 约束和生成表达式可能引用 tableoid 列，
	 * 因此在评估它们之前（重新）初始化 tts_tableOid。
	 */
	fc_slot->tts_tableOid = RelationGetRelid(fc_resultRelationDesc);

	/*
	 * 计算存储生成列
	 */
	if (fc_resultRelationDesc->rd_att->constr &&
		fc_resultRelationDesc->rd_att->constr->has_generated_stored)
		ExecComputeStoredGenerated(fc_resultRelInfo, fc_estate, fc_slot,
								   CMD_UPDATE);
}

/*
 * ExecUpdateAct -- ExecUpdate 的子例程
 *
 * 在操作一个普通表时实际更新元组。如果表是一个分区，并且命令是
 * 参考一个祖先分区表被调用的，则此例程将结果元组迁移到另一个
 * 分区。
 *
 * 调用者负责根据需要保持索引的最新状态。调用者还负责进行 EvalPlanQual，
 * 如果发现元组被并发更新。然而，在跨分区更新的情况下，此例程会处理此事。
 *
 * 调用者负责根据需要进行 EvalPlanQual，并保持更新的索引最新。
 */
static TM_Result fc_ExecUpdateAct(ModifyTableContext *fc_context, ResultRelInfo *fc_resultRelInfo,
			  ItemPointer fc_tupleid, HeapTuple fc_oldtuple, TupleTableSlot *fc_slot,
			  bool fc_canSetTag, UpdateContext *fc_updateCxt)
{
	EState	   *fc_estate = fc_context->estate;
	Relation	fc_resultRelationDesc = fc_resultRelInfo->ri_RelationDesc;
	bool		fc_partition_constraint_failed;
	TM_Result	fc_result;

	fc_updateCxt->crossPartUpdate = false;

	/*
	 * 如果我们将元组移动到新分区，我们将在这里循环以重新计算
	 * 生成值（在分区间可能不同）并重新检查任何 RLS 策略和约束。
	 * 然而，我们不会触发新分区的任何行前触发器。
	 */
lreplace:
	/* 填充生成的列 */
	fc_ExecUpdatePrepareSlot(fc_resultRelInfo, fc_slot, fc_estate);

	/* 确保槽是独立的，考虑例如 EPQ */
	ExecMaterializeSlot(fc_slot);

	/*
	 * 如果分区约束失败，这行可能会被移动到另一个分区，
	 * 在这种情况下，我们应该在插入新分区之前检查 RLS CHECK 策略，
	 * 而不是在这里执行。这是因为该分区上的触发器可能再次更改该行。
	 * 因此，如果分区约束失败，则跳过 WCO 检查。
	 */
	fc_partition_constraint_failed =
		fc_resultRelationDesc->rd_rel->relispartition &&
		!ExecPartitionCheck(fc_resultRelInfo, fc_slot, fc_estate, false);

	/* 检查任何 RLS 更新检查策略 */
	if (!fc_partition_constraint_failed &&
		fc_resultRelInfo->ri_WithCheckOptions != NIL)
	{
		/*
		 * ExecWithCheckOptions() 将跳过不是我们此时所寻找的类型的 WCO。
		 */
		ExecWithCheckOptions(WCO_RLS_UPDATE_CHECK,
							 fc_resultRelInfo, fc_slot, fc_estate);
	}

	/*
	 * 如果分区检查失败，尝试将行移动到正确的分区。
	 */
	if (fc_partition_constraint_failed)
	{
		TupleTableSlot *fc_inserted_tuple,
				   *fc_retry_slot;
		ResultRelInfo *insert_destrel = NULL;

		/*
		 * ExecCrossPartitionUpdate 首先会从其当前所在的分区删除该行，
		 * 然后将其插入回根表，这将重新路由到正确的分区。
		 * 然而，如果元组已被并发更新，则需要重试。
		 */
		if (fc_ExecCrossPartitionUpdate(fc_context, fc_resultRelInfo,
									 fc_tupleid, fc_oldtuple, fc_slot,
									 fc_canSetTag, fc_updateCxt,
									 &fc_result,
									 &fc_retry_slot,
									 &fc_inserted_tuple,
									 &insert_destrel))
		{
			/* 成功！ */
			fc_updateCxt->updated = true;
			fc_updateCxt->crossPartUpdate = true;

			
/*
			 * 如果被更新的分区表在外键中被引用，安排触发器事件以检查是否有违反的情况。 在非跨分区更新的情况下不需要特殊处理，因为叶子分区的 AR 更新触发器会处理这个问题。 在实施为在源分区上删除后在目标分区上插入的跨分区更新时，必须触发根表（即查询中提到的表）的 AR-UPDATE 触发器。
			 *
			 * NULL insert_destrel 表示移动未能发生，即更新失败，因此在这种情况下无需处理任何事情。
			 */
			if (insert_destrel &&
				fc_resultRelInfo->ri_TrigDesc &&
				fc_resultRelInfo->ri_TrigDesc->trig_update_after_row)
				fc_ExecCrossPartitionUpdateForeignKey(fc_context,
												   fc_resultRelInfo,
												   insert_destrel,
												   fc_tupleid, fc_slot,
												   fc_inserted_tuple);

			return TM_Ok;
		}

		/*
		 * 运气不好，需要重试。如果运行 MERGE，这里不做；而是让它根据自己的规则处理。
		 */
		if (fc_context->relaction != NULL)
			return fc_result;

		/*
		 * ExecCrossPartitionUpdate 在重试槽中安装了新元组的更新版本；重新开始。
		 */
		fc_slot = fc_retry_slot;
		goto lreplace;
	}

	/*
	 * 检查元组的约束条件。我们已经在上面检查了分区约束；然而，我们仍然必须确保元组通过所有其他约束，因此我们将调用 ExecConstraints() 并让它验证所有剩余的检查。
	 */
	if (fc_resultRelationDesc->rd_att->constr)
		ExecConstraints(fc_resultRelInfo, fc_slot, fc_estate);

	/*
	 * 替换堆元组
	 *
	 * 注意：如果 es_crosscheck_snapshot 不是 InvalidSnapshot，我们检查要更新的行在该快照中是否可见，如果不可见则抛出无法序列化的错误。这是事务快照模式事务中需要的引用完整性更新的特殊情况行为。
	 */
	fc_result = table_tuple_update(fc_resultRelationDesc, fc_tupleid, fc_slot,
								fc_estate->es_output_cid,
								fc_estate->es_snapshot,
								fc_estate->es_crosscheck_snapshot,
								true /* 等待提交 */ ,
								&fc_context->tmfd, &fc_updateCxt->lockmode,
								&fc_updateCxt->updateIndexes);
	if (fc_result == TM_Ok)
		fc_updateCxt->updated = true;

	return fc_result;
}

/*
 * ExecUpdateEpilogue -- ExecUpdate 的子例程
 *
 * 更新元组的最后步骤。如果 ExecUpdateAct 返回指示元组已更新，则必须调用。
 */
static void fc_ExecUpdateEpilogue(ModifyTableContext *fc_context, UpdateContext *fc_updateCxt,
				   ResultRelInfo *fc_resultRelInfo, ItemPointer fc_tupleid,
				   HeapTuple fc_oldtuple, TupleTableSlot *fc_slot)
{
	ModifyTableState *fc_mtstate = fc_context->mtstate;
	List	   *fc_recheckIndexes = NIL;

	/* 如果必要，插入元组的索引条目 */
	if (fc_resultRelInfo->ri_NumIndices > 0 && fc_updateCxt->updateIndexes)
		fc_recheckIndexes = ExecInsertIndexTuples(fc_resultRelInfo,
											   fc_slot, fc_context->estate,
											   true, false,
											   NULL, NIL);

	/* 行更新之后触发器 */
	ExecARUpdateTriggers(fc_context->estate, fc_resultRelInfo,
						 NULL, NULL,
						 fc_tupleid, fc_oldtuple, fc_slot,
						 fc_recheckIndexes,
						 fc_mtstate->operation == CMD_INSERT ?
						 fc_mtstate->mt_oc_transition_capture :
						 fc_mtstate->mt_transition_capture,
						 false);

	list_free(fc_recheckIndexes);

	/*
	 * 检查父视图中的任何 WITH CHECK OPTION 约束。根据 SQL 规范，我们
	 * 要求在测试所有约束和唯一性冲突后进行此操作，因此我们在实际更新
	 * 堆及所有索引中的记录之后再执行此操作。
	 *
	 * ExecWithCheckOptions() 将跳过此时我们所寻找的不是的 WCO。
	 */
	if (fc_resultRelInfo->ri_WithCheckOptions != NIL)
		ExecWithCheckOptions(WCO_VIEW_CHECK, fc_resultRelInfo,
							 fc_slot, fc_context->estate);
}

/*
 * 使用目标根分区表的触发器排队更新事件，以检查跨分区更新是否
 * 未破坏指向它的任何外键。
 */
static void fc_ExecCrossPartitionUpdateForeignKey(ModifyTableContext *fc_context,
								   ResultRelInfo *fc_sourcePartInfo,
								   ResultRelInfo *fc_destPartInfo,
								   ItemPointer fc_tupleid,
								   TupleTableSlot *fc_oldslot,
								   TupleTableSlot *fc_newslot)
{
	ListCell   *fc_lc;
	ResultRelInfo *fc_rootRelInfo;
	List	   *fc_ancestorRels;

	fc_rootRelInfo = fc_sourcePartInfo->ri_RootResultRelInfo;
	fc_ancestorRels = ExecGetAncestorResultRels(fc_context->estate, fc_sourcePartInfo);

	/*
	 * 对于直接指向源分区的非根祖先的任何外键，理论上我们可以触发
	 * 更新事件以使用它们的触发器强制执行这些约束，前提是我们能
	 * 确定源分区和目标分区都位于同一祖先之下。但目前，我们只是报
	 * 告无法强制执行这些约束的错误。
	 */
	foreach(fc_lc, fc_ancestorRels)
	{
		ResultRelInfo *fc_rInfo = lfirst(fc_lc);
		TriggerDesc *fc_trigdesc = fc_rInfo->ri_TrigDesc;
		bool		fc_has_noncloned_fkey = false;

		/* 根祖先的触发器将被处理。 */
		if (fc_rInfo == fc_rootRelInfo)
			continue;

		if (fc_trigdesc && fc_trigdesc->trig_update_after_row)
		{
			for (int fc_i = 0; fc_i < fc_trigdesc->numtriggers; fc_i++)
			{
				Trigger    *fc_trig = &fc_trigdesc->triggers[fc_i];

				if (!fc_trig->tgisclone &&
					RI_FKey_trigger_type(fc_trig->tgfoid) == RI_TRIGGER_PK)
				{
					fc_has_noncloned_fkey = true;
					break;
				}
			}
		}

		if (fc_has_noncloned_fkey)
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
					 errmsg("cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key"),
					 errdetail("A foreign key points to ancestor \"%s\" but not the root ancestor \"%s\".",
							   RelationGetRelationName(fc_rInfo->ri_RelationDesc),
							   RelationGetRelationName(fc_rootRelInfo->ri_RelationDesc)),
					 errhint("Consider defining the foreign key on table \"%s\".",
							 RelationGetRelationName(fc_rootRelInfo->ri_RelationDesc))));
	}

	/* 执行根表的触发器。 */
	ExecARUpdateTriggers(fc_context->estate,
						 fc_rootRelInfo, fc_sourcePartInfo, fc_destPartInfo,
						 fc_tupleid, NULL, fc_newslot, NIL, NULL, true);
}

/* ----------------------------------------------------------------
 *		ExecUpdate
 *
 *		注意：我们无法在关闭事务的情况下运行 UPDATE 查询，
 *		因为 UPDATE 实际上是 INSERT，而我们的扫描将错误地
 *		无限循环，不断更新刚刚插入的元组。这个问题应该被修复，
 *		但在修复之前，我们不希望陷入破坏您数据库的无限循环中。
 *
 *		更新表时，tupleid 标识要更新的元组，而 oldtuple 为 NULL。
 *		通过视图的 INSTEAD OF 触发器进行更新时，将 oldtuple 传递
 *		给触发器，用于标识要更新的内容，而 tupleid 无效。
 *		在更新外部表时，tupleid 是无效的；FDW 必须使用来自
 *		planSlot 的数据来确定要更新的行。oldtuple 被传递给
 *		外部表触发器；当外部表没有相关触发器时为 NULL。
 *
 *		slot 包含要存储的新元组值。
 *		planSlot 是 ModifyTable 的子计划的输出；我们使用它
 *		访问其他输入表的值（对于 RETURNING）、行-ID 垃圾列等。
 *
 *		如果有 RETURNING 结果，则返回它，否则返回 NULL。
 *		退出时，如果 tupleid 已识别要更新的元组，
 *		则它将识别在 EvalPlanQual 后实际更新的元组。
 * ----------------------------------------------------------------
 */
static TupleTableSlot * fc_ExecUpdate(ModifyTableContext *fc_context, ResultRelInfo *fc_resultRelInfo,
		   ItemPointer fc_tupleid, HeapTuple fc_oldtuple, TupleTableSlot *fc_slot,
		   bool fc_canSetTag)
{
	EState	   *fc_estate = fc_context->estate;
	Relation	fc_resultRelationDesc = fc_resultRelInfo->ri_RelationDesc;
	UpdateContext fc_updateCxt = {0};
	TM_Result	fc_result;

	/*
	 * 如果未运行事务则中止操作
	 */
	if (IsBootstrapProcessingMode())
		elog(ERROR, "cannot UPDATE during bootstrap");

	/*
	 * 准备进行更新。这包括 BEFORE ROW 触发器，因此如果
	 * 它说我们完成了，那我们就完成了。
	 */
	if (!fc_ExecUpdatePrologue(fc_context, fc_resultRelInfo, fc_tupleid, fc_oldtuple, fc_slot, NULL))
		return NULL;

	/* INSTEAD OF ROW UPDATE 触发器 */
	if (fc_resultRelInfo->ri_TrigDesc &&
		fc_resultRelInfo->ri_TrigDesc->trig_update_instead_row)
	{
		if (!ExecIRUpdateTriggers(fc_estate, fc_resultRelInfo,
								  fc_oldtuple, fc_slot))
			return NULL;		/* "不做任何操作" */
	}
	else if (fc_resultRelInfo->ri_FdwRoutine)
	{
		/* 填充生成的列 */
		fc_ExecUpdatePrepareSlot(fc_resultRelInfo, fc_slot, fc_estate);

		/*
		 * 在外部表中进行更新：让 FDW 来处理
		 */
		fc_slot = fc_resultRelInfo->ri_FdwRoutine->ExecForeignUpdate(fc_estate,
															   fc_resultRelInfo,
															   fc_slot,
															   fc_context->planSlot);

		if (fc_slot == NULL)		/* "不做任何操作" */
			return NULL;

		/*
		 * AFTER ROW触发器或RETURNING表达式可能会引用tableoid列，因此在评估它们之前重新初始化tts_tableOid。
		 * （这覆盖了FDW替换了槽的情况。）
		 */
		fc_slot->tts_tableOid = RelationGetRelid(fc_resultRelationDesc);
	}
	else
	{
		ItemPointerData fc_lockedtid;

		/*
		 * 如果我们在 EvalPlanQual 测试后生成了新的候选元组，
		 * 我们必须返回这里重试。不过，我们不需要重新执行触发器。
		 * 如果有任何 BEFORE 触发器，则 trigger.c 将进行了
		 * table_tuple_lock 以锁定正确的元组，因此无需
		 * 再次执行。
		 */
redo_act:
		fc_lockedtid = *fc_tupleid;
		fc_result = fc_ExecUpdateAct(fc_context, fc_resultRelInfo, fc_tupleid, fc_oldtuple, fc_slot,
							   fc_canSetTag, &fc_updateCxt);

		/*
		 * 如果 ExecUpdateAct 报告跨分区更新已完成，
		 * 则 RETURNING 元组（如果有）已被投影，没有其他
		 * 工作要做。
		 */
		if (fc_updateCxt.crossPartUpdate)
			return fc_context->cpUpdateReturningSlot;

		switch (fc_result)
		{
			case TM_SelfModified:

				/*
				 * 目标元组已被当前命令或当前事务中的
				 * 后续命令更新或删除。前一种情况在多个元组
				 * 连接到同一目标元组的连接 UPDATE 中是可能的。
				 * 这相当有争议，但 PostgreSQL 一直允许这种情况：
				 * 我们只需执行第一个更新动作并忽略
				 * 额外的更新尝试。
				 *
				 * 后一种情况发生在元组被 BEFORE 触发器中的
				 * 命令修改时，或者可能被查询中使用的
				 * 易失性函数中的命令修改。在这种情况下，我们
				 * 不应忽略更新，但继续也是同样不安全的。
				 * 我们不想在保留基于此的触发动作的同时丢弃
				 * 原始 UPDATE；我们也没有合理的方法将此更新
				 * 与以前的更新合并。因此，抛出错误是唯一安全的
				 * 做法。
				 *
				 * 如果触发器确实打算进行这种类型的交互，
				 * 它可以重新执行 UPDATE（假设它能搞清楚
				 * 如何），然后返回 NULL 以取消外部更新。
				 */
				if (fc_context->tmfd.cmax != fc_estate->es_output_cid)
					ereport(ERROR,
							(errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
							 errmsg("tuple to be updated was already modified by an operation triggered by the current command"),
							 errhint("Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.")));

				/* 否则，已被自身更新；无需操作 */
				return NULL;

			case TM_Ok:
				break;

			case TM_Updated:
				{
					TupleTableSlot *fc_inputslot;
					TupleTableSlot *fc_epqslot;
					TupleTableSlot *fc_oldSlot;

					if (IsolationUsesXactSnapshot())
						ereport(ERROR,
								(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
								 errmsg("could not serialize access due to concurrent update")));

					/*
					 * 已经知道我们需要执行 EPQ，因此直接将元组抓取到
					 * 正确的槽中。
					 */
					fc_inputslot = EvalPlanQualSlot(fc_context->epqstate, fc_resultRelationDesc,
												 fc_resultRelInfo->ri_RangeTableIndex);

					fc_result = table_tuple_lock(fc_resultRelationDesc, fc_tupleid,
											  fc_estate->es_snapshot,
											  fc_inputslot, fc_estate->es_output_cid,
											  fc_updateCxt.lockmode, LockWaitBlock,
											  TUPLE_LOCK_FLAG_FIND_LAST_VERSION,
											  &fc_context->tmfd);

					switch (fc_result)
					{
						case TM_Ok:
							Assert(fc_context->tmfd.traversed);

							fc_epqslot = EvalPlanQual(fc_context->epqstate,
												   fc_resultRelationDesc,
												   fc_resultRelInfo->ri_RangeTableIndex,
												   fc_inputslot);
							if (TupIsNull(fc_epqslot))
								/* 元组不再通过资格，正在退出... */
								return NULL;

							/* 确保 ri_oldTupleSlot 已初始化。 */
							if (unlikely(!fc_resultRelInfo->ri_projectNewInfoValid))
								fc_ExecInitUpdateProjection(fc_context->mtstate,
														 fc_resultRelInfo);

							if (fc_resultRelInfo->ri_needLockTagTuple)
							{
								UnlockTuple(fc_resultRelationDesc,
											&fc_lockedtid, InplaceUpdateTupleLock);
								LockTuple(fc_resultRelationDesc,
										  fc_tupleid, InplaceUpdateTupleLock);
							}

							/* 获取旧元组的最新版本。 */
							fc_oldSlot = fc_resultRelInfo->ri_oldTupleSlot;
							if (!table_tuple_fetch_row_version(fc_resultRelationDesc,
															   fc_tupleid,
															   SnapshotAny,
															   fc_oldSlot))
								elog(ERROR, "failed to fetch tuple being updated");
							fc_slot = ExecGetUpdateNewTuple(fc_resultRelInfo,
														 fc_epqslot, fc_oldSlot);
							goto redo_act;

						case TM_Deleted:
							/* 元组已经被删除；无事可做 */
							return NULL;

						case TM_SelfModified:

							/*
							 * 当沿着从其他会话更新的元组的更新链时，
							 * 达到在此事务中已经更新的元组时，可以到达此点。
							 * 如果以前被此命令修改，则忽略冗余的更新，
							 * 否则返回错误。
							 *
							 * 另见上面 table_tuple_update() 的 TM_SelfModified 响应。
							 */
							if (fc_context->tmfd.cmax != fc_estate->es_output_cid)
								ereport(ERROR,
										(errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
										 errmsg("tuple to be updated was already modified by an operation triggered by the current command"),
										 errhint("Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.")));
							return NULL;

						default:
							/* 查看 ExecDelete() 中的 table_tuple_lock 调用 */
							elog(ERROR, "unexpected table_tuple_lock status: %u",
								 fc_result);
							return NULL;
					}
				}

				break;

			case TM_Deleted:
				if (IsolationUsesXactSnapshot())
					ereport(ERROR,
							(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
							 errmsg("could not serialize access due to concurrent delete")));
				/* 元组已经被删除；无事可做 */
				return NULL;

			default:
				elog(ERROR, "unrecognized table_tuple_update status: %u",
					 fc_result);
				return NULL;
		}
	}

	if (fc_canSetTag)
		(fc_estate->es_processed)++;

	fc_ExecUpdateEpilogue(fc_context, &fc_updateCxt, fc_resultRelInfo, fc_tupleid, fc_oldtuple,
					   fc_slot);

	/* 如果存在，处理 RETURNING */
	if (fc_resultRelInfo->ri_projectReturning)
		return fc_ExecProcessReturning(fc_resultRelInfo, fc_slot, fc_context->planSlot);

	return NULL;
}

/*
 * ExecOnConflictUpdate --- 执行 INSERT ON CONFLICT DO UPDATE 的 UPDATE
 *
 * 尝试锁定元组以作为投机性插入的一部分进行更新。如果
 * 来自 ON CONFLICT DO UPDATE 的条件被满足，则进行更新
 * （但仍然锁定行，即使它可能不满足状态的
 * 快照）。
 *
 * 如果我们完成了（有或没有更新），则返回 true，
 * 如果调用者必须从头开始重试 INSERT，则返回 false。
 */
static bool fc_ExecOnConflictUpdate(ModifyTableContext *fc_context,
					 ResultRelInfo *fc_resultRelInfo,
					 ItemPointer fc_conflictTid,
					 TupleTableSlot *fc_excludedSlot,
					 bool fc_canSetTag,
					 TupleTableSlot **fc_returning)
{
	ModifyTableState *fc_mtstate = fc_context->mtstate;
	ExprContext *fc_econtext = fc_mtstate->ps.ps_ExprContext;
	Relation	fc_relation = fc_resultRelInfo->ri_RelationDesc;
	ExprState  *fc_onConflictSetWhere = fc_resultRelInfo->ri_onConflict->oc_WhereClause;
	TupleTableSlot *fc_existing = fc_resultRelInfo->ri_onConflict->oc_Existing;
	TM_FailureData fc_tmfd;
	LockTupleMode fc_lockmode;
	TM_Result	fc_test;
	Datum		fc_xminDatum;
	TransactionId fc_xmin;
	bool		fc_isnull;

	/*
	 * 解析分析应该阻止所有系统
	 * 关系的 ON CONFLICT，包括这些。支持此项没有根本性的障碍；
	 * 我们只需像其他 ExecUpdate() 调用者一样处理 LOCKTAG_TUPLE。
	 */
	Assert(!fc_resultRelInfo->ri_needLockTagTuple);

	/* 确定要使用的锁模式 */
	fc_lockmode = ExecUpdateLockMode(fc_context->estate, fc_resultRelInfo);

	/*
	 * 锁定元组以进行更新。当无法在不执行的情况下锁定元组时，不要跟随更新。
	 * 这里的行锁定冲突意味着我们之前得出的结论，即元组已确凿提交，已经不再成立。
	 */
	fc_test = table_tuple_lock(fc_relation, fc_conflictTid,
							fc_context->estate->es_snapshot,
							fc_existing, fc_context->estate->es_output_cid,
							fc_lockmode, LockWaitBlock, 0,
							&fc_tmfd);
	switch (fc_test)
	{
		case TM_Ok:
			/* 成功！ */
			break;

		case TM_Invisible:

			/*
			 * 当在同一命令中对刚插入的元组再次进行更新时，可以发生此情况。
			 * 例如，因为插入了具有相同冲突键值的多行。
			 *
			 * 这在某种程度上类似于 ExecUpdate() TM_SelfModified
			 * 情况。我们不想继续，因为这将导致
			 * 同一行以某种未指定的顺序第二次被更新，
			 * 而且与普通的 UPDATE 不同，没有历史行为
			 * 需要打破。
			 *
			 * 防止这种情况发生是用户的责任。这些问题就是为什么 SQL 标准同样
			 * 指定，对于 SQL MERGE，在尝试两次更新同一行时必须引发异常。
			 */
			fc_xminDatum = slot_getsysattr(fc_existing,
										MinTransactionIdAttributeNumber,
										&fc_isnull);
			Assert(!fc_isnull);
			fc_xmin = DatumGetTransactionId(fc_xminDatum);

			if (TransactionIdIsCurrentTransactionId(fc_xmin))
				ereport(ERROR,
						(errcode(ERRCODE_CARDINALITY_VIOLATION),
				/* 翻译者：%s 是一个 SQL 命令名 */
						 errmsg("%s command cannot affect row a second time",
								"ON CONFLICT DO UPDATE"),
						 errhint("Ensure that no rows proposed for insertion within the same command have duplicate constrained values.")));

			/* 这不应该发生 */
			elog(ERROR, "attempted to lock invisible tuple");
			break;

		case TM_SelfModified:

			/*
			 * 该状态永远不应达到。由于使用
			 * 脏快照来查找冲突的元组，投机性插入不会
			 * 看到这个行以产生冲突。
			 */
			elog(ERROR, "unexpected self-updated tuple");
			break;

		case TM_Updated:
			if (IsolationUsesXactSnapshot())
				ereport(ERROR,
						(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
						 errmsg("could not serialize access due to concurrent update")));

			/*
			 * 只要我们不支持对分区表的 INSERT ON CONFLICT 的 UPDATE，
			 * 我们就不应达到因并发更新分区键而导致的需锁定的元组被移动到另一个分区的情况。
			 */
			Assert(!ItemPointerIndicatesMovedPartitions(&fc_tmfd.ctid));

			/*
			 * 告诉调用者从头再试一次。
			 *
			 * 在这里使用通常的 EvalPlanQual() 风格循环是没有意义的，
			 * 因为行的新版本可能不再冲突，
			 * 或冲突的元组实际上已被删除。
			 */
			ExecClearTuple(fc_existing);
			return false;

		case TM_Deleted:
			if (IsolationUsesXactSnapshot())
				ereport(ERROR,
						(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
						 errmsg("could not serialize access due to concurrent delete")));

			/* 见 TM_Updated 情况 */
			Assert(!ItemPointerIndicatesMovedPartitions(&fc_tmfd.ctid));
			ExecClearTuple(fc_existing);
			return false;

		default:
			elog(ERROR, "unrecognized table_tuple_lock status: %u", fc_test);
	}

	/* 成功，元组被锁定。 */

	/*
	 * 验证该元组是否在我们的 MVCC 快照中可见，如果当前
	 * 隔离级别要求这样做。
	 *
	 * 仅依赖于 ExecUpdate() 内的检查是不够的，例如
	 * CONFLICT ... WHERE 子句可能会阻止我们达到这一点。
	 *
	 * 这意味着我们只有在当前
	 * 事务中的新命令可以看到该行时才继续，即使在 READ COMMITTED 模式下
	 * 根据当前语句的快照，该元组将不可见。这与
	 * UPDATE 处理新元组版本的方式是一致的。
	 */
	fc_ExecCheckTupleVisible(fc_context->estate, fc_relation, fc_existing);

	/*
	 * 使元组和任何必要的连接变量对 ExecQual 和
	 * ExecProject 可用。EXCLUDED 元组安装在 ecxt_innertuple 中，
	 * 而目标的现有元组安装在 scantuple 中。EXCLUDED
	 * 已经被设置为引用 setrefs.c 中的 INNER_VAR，但没有
	 * 其他重定向。
	 */
	fc_econtext->ecxt_scantuple = fc_existing;
	fc_econtext->ecxt_innertuple = fc_excludedSlot;
	fc_econtext->ecxt_outertuple = NULL;

	if (!ExecQual(fc_onConflictSetWhere, fc_econtext))
	{
		ExecClearTuple(fc_existing);	/* 见下面的返回 */
		InstrCountFiltered1(&fc_mtstate->ps, 1);
		return true;			/* 完成元组处理 */
	}

	if (fc_resultRelInfo->ri_WithCheckOptions != NIL)
	{
		/*
		 * 检查目标的现有元组是否适用于 UPDATE 的 USING
		 * 安全壁垒资格（如果有），在这里作为 RLS 检查/WCO 强制执行。
		 *
		 * 重写器为 UPDATE 安全
		 * 资格创建 UPDATE RLS 检查/WCO，并将它们存储为 WCO 类型
		 * WCO_RLS_CONFLICT_CHECK，但这几乎是它针对 ON
		 * CONFLICT DO UPDATE 的特殊处理的全部内容。
		 *
		 * 重写器还会关联适用于 UPDATE 的直接
		 * RLS 检查/WCO，以便于后续的 ExecUpdate() 调用。
		 * INSERT 和 UPDATE 自然具有相互排斥的 WCO
		 * 类型，因此在 INSERT 或 UPDATE 路径中没有虚假的过度强制执行的危险。
		 */
		ExecWithCheckOptions(WCO_RLS_CONFLICT_CHECK, fc_resultRelInfo,
							 fc_existing,
							 fc_mtstate->ps.state);
	}

	/* 投影新元组版本 */
	ExecProject(fc_resultRelInfo->ri_onConflict->oc_ProjInfo);

	/*
	 * 注意，目标元组可能在此会话中已被修改，
	 * 在上述 table_tuple_lock 之后。如果发生这种情况，我们选择不报错，
	 * 这与 ExecUpdate 对类似情况的处理一致。
	 * 如果从 ExecQual()、ExecWithCheckOptions()
	 * 或 ExecProject() 以上触发了一个 UPDATE，这种情况可能会发生，
	 * 例如，从 ON CONFLICT 的 SET 中选择 wCTE。
	 */

	/* 执行具有投影的 UPDATE */
	*fc_returning = fc_ExecUpdate(fc_context, fc_resultRelInfo,
							fc_conflictTid, NULL,
							fc_resultRelInfo->ri_onConflict->oc_ProjSlot,
							fc_canSetTag);

	/*
	 * 清除现有元组，因为下一组输入行中可能没有其他冲突。
	 * 不希望资源保持到查询结束。
	 */
	ExecClearTuple(fc_existing);
	return true;
}

/*
 * 执行 MERGE。
 */
static TupleTableSlot * fc_ExecMerge(ModifyTableContext *fc_context, ResultRelInfo *fc_resultRelInfo,
		  ItemPointer fc_tupleid, bool fc_canSetTag)
{
	bool		fc_matched;

	/*-----
	 * 如果我们处理的是 WHEN MATCHED 情况（tupleid 有效），
	 * 我们执行第一个通过附加 WHEN MATCHED AND
	 * 资格的操作。如果找到没有资格的操作，则执行该操作。
	 *
	 * 类似地，如果我们处理的是 WHEN NOT MATCHED 情况，
	 * 我们按顺序查看给定的 WHEN NOT MATCHED 操作，直到找到一个通过的。
	 *
	 * 在目标元组的并发更新/删除情况下，情况变得有趣。
	 * 在执行 WHEN MATCHED 操作时，检测到此类并发更新/删除。
	 *
	 * 并发更新可以：
	 *
	 * 1. 修改目标元组，以使其不再满足
	 *    附加到当前 WHEN MATCHED 操作的资格。
	 *
	 *    在这种情况下，我们仍在处理 WHEN MATCHED 情况。
	 *    我们会从头重新检查 WHEN MATCHED 操作的列表，
	 *    并选择第一个满足新目标元组的操作。
	 *
	 * 2. 修改目标元组，以使连接资格不再通过，
	 *    因此源元组不再匹配。
	 *
	 *    在这种情况下，源元组不再匹配目标元组，
	 *    所以我们现在寻找一个合格的 WHEN NOT MATCHED 操作来执行。
	 *
	 * XXX 嗯，如果更新后的元组现在会与
	 * 之前被认为是 NOT MATCHED 的元组匹配呢？
	 *
	 * 并发删除将 WHEN MATCHED 情况转换为 WHEN NOT MATCHED。
	 *
	 * ExecMergeMatched 负责跟踪更新链并
	 * 重新找到合格的 WHEN MATCHED 操作，只要更新后的
	 * 目标元组仍满足连接资格，即它仍然是 WHEN
	 * MATCHED 情况。如果元组被删除或连接资格失败，
	 * 它将返回，我们尝试 ExecMergeNotMatched。考虑到 ExecMergeMatched
	 * 始终通过跟踪更新链取得进展，并且我们从未将
	 * ExecMergeNotMatched 切换到 ExecMergeMatched，因此没有发生
	 * 活锁的风险。
	 */
	fc_matched = fc_tupleid != NULL;
	if (fc_matched)
		fc_matched = fc_ExecMergeMatched(fc_context, fc_resultRelInfo, fc_tupleid, fc_canSetTag);

	/*
	 * 要么我们正在处理 NOT MATCHED 元组，要么 ExecMergeMatched()
	 * 返回“false”，表示之前匹配的元组不再匹配。
	 */
	if (!fc_matched)
		fc_ExecMergeNotMatched(fc_context, fc_resultRelInfo, fc_canSetTag);

	/* 还没有 RETURNING 支持 */
	return NULL;
}

/*
 * 检查并执行第一个合格的 MATCHED 操作。当前的目标
 * 元组通过 tupleid 确定。
 *
 * 我们从第一个 WHEN MATCHED 操作开始，检查 WHEN 资格
 * 是否通过（如果有）。如果第一个操作的 WHEN 资格不通过，
 * 我们检查第二个，然后第三个，依此类推。如果到达末尾，不
 * 采取任何行动，并返回 true，表示该元组无需进一步处理。
 *
 * 如果找到合格的操作，我们将尝试执行该操作。
 *
 * 如果元组被并发更新，将使用更新后的
 * 元组运行 EvalPlanQual 以重新检查连接资格。
 * 注意，单个操作相关的附加资格由此例程通过
 * ExecQual 评估，而 EvalPlanQual 检查连接资格。
 * 如果 EvalPlanQual 告诉我们更新后的元组仍通过
 * 连接资格，则我们从第一个操作重新开始，寻找合格的操作。
 * 否则，我们返回 false —— 意味着现在必须为当前
 * 源元组执行一个 NOT MATCHED 操作。
 */
static bool fc_ExecMergeMatched(ModifyTableContext *fc_context, ResultRelInfo *fc_resultRelInfo,
				 ItemPointer fc_tupleid, bool fc_canSetTag)
{
	ModifyTableState *fc_mtstate = fc_context->mtstate;
	ItemPointerData fc_lockedtid;
	TupleTableSlot *fc_newslot;
	EState	   *fc_estate = fc_context->estate;
	ExprContext *fc_econtext = fc_mtstate->ps.ps_ExprContext;
	bool		fc_isNull;
	EPQState   *fc_epqstate = &fc_mtstate->mt_epqstate;
	ListCell   *fc_l;
	bool		fc_no_further_action = true;

	/*
	 * 如果没有 WHEN MATCHED 操作，我们就完成了。
	 */
	if (fc_resultRelInfo->ri_matchedMergeAction == NIL)
		return fc_no_further_action;

	
/*
	 * 将元组和任何需要的连接变量提供给 ExecQual 和
	 * ExecProject。目标的现有元组被安装在 scantuple 中。
	 * 再次强调，这个目标关系的槽仅在匹配元组和更新/删除操作的情况下是必需的。
	 */
	fc_econtext->ecxt_scantuple = fc_resultRelInfo->ri_oldTupleSlot;
	fc_econtext->ecxt_innertuple = fc_context->planSlot;
	fc_econtext->ecxt_outertuple = NULL;

lmerge_matched:;

	if (fc_resultRelInfo->ri_needLockTagTuple)
	{
		/*
		 * 这甚至对 CMD_DELETE、CMD_NOTHING 和不匹配 mas_whenqual 的元组也会加锁。
		 * 在系统目录上的合并是一个小用例，因此不必优化这些。
		 */
		LockTuple(fc_resultRelInfo->ri_RelationDesc, fc_tupleid,
				  InplaceUpdateTupleLock);
		fc_lockedtid = *fc_tupleid;
	}
	else
		ItemPointerSetInvalid(&fc_lockedtid);

	/*
	 * 这个例程仅在匹配行时调用，并且在这种情况下，我们必须找到目标行的 tupleid；取回该元组。
	 *
	 * 我们使用 SnapshotAny，因为在 EvalPlanQual 返回一个新的元组后，可能会再次被调用，
	 * 而这个新的元组可能对我们的 MVCC 快照不可见。
	 */

	if (!table_tuple_fetch_row_version(fc_resultRelInfo->ri_RelationDesc,
									   fc_tupleid,
									   SnapshotAny,
									   fc_resultRelInfo->ri_oldTupleSlot))
		elog(ERROR, "failed to fetch the target tuple");

	foreach(fc_l, fc_resultRelInfo->ri_matchedMergeAction)
	{
		MergeActionState *fc_relaction = (MergeActionState *) lfirst(fc_l);
		CmdType		fc_commandType = fc_relaction->mas_action->commandType;
		TM_Result	fc_result;
		UpdateContext fc_updateCxt = {0};

		/*
		 * 测试条件（如果有的话）。
		 *
		 * 在没有任何条件的情况下，我们执行该操作
		 * 不加条件（无需单独检查，因为如果没有条件评估，ExecQual() 将返回 true）。
		 */
		if (!ExecQual(fc_relaction->mas_whenqual, fc_econtext))
			continue;

		/*
		 * 检查现有目标元组是否满足更新/删除 RLS 策略的 USING 检查。
		 * 如果这些检查失败，我们将抛出一个错误。
		 *
		 * UPDATE RLS 策略的 WITH CHECK quals 在 ExecUpdateAct() 中应用，
		 * 因此我们不需要做任何特别处理。
		 *
		 * 注意：我们必须在 WHEN quals 评估后进行此操作，以便我们
		 * 仅在它们有意义时检查策略。
		 */
		if (fc_resultRelInfo->ri_WithCheckOptions && fc_commandType != CMD_NOTHING)
		{
			ExecWithCheckOptions(fc_commandType == CMD_UPDATE ?
								 WCO_RLS_MERGE_UPDATE_CHECK : WCO_RLS_MERGE_DELETE_CHECK,
								 fc_resultRelInfo,
								 fc_resultRelInfo->ri_oldTupleSlot,
								 fc_context->mtstate->ps.state);
		}

		/* 执行声明的操作 */
		switch (fc_commandType)
		{
			case CMD_UPDATE:

				/*
				 * 投影输出元组，并用它来更新表。
				 * 我们不需要过滤掉无用的属性，因为
				 * 更新操作的目标列表中没有任何属性。
				 */
				fc_newslot = ExecProject(fc_relaction->mas_proj);

				fc_context->relaction = fc_relaction;
				if (!fc_ExecUpdatePrologue(fc_context, fc_resultRelInfo,
										fc_tupleid, NULL, fc_newslot, &fc_result))
				{
					if (fc_result == TM_Ok)
						goto out;	/* "不做任何操作" */
					break;		/* 并发更新/删除 */
				}
				fc_result = fc_ExecUpdateAct(fc_context, fc_resultRelInfo, fc_tupleid, NULL,
									   fc_newslot, fc_canSetTag, &fc_updateCxt);

				/*
				 * 与 ExecUpdate() 中一样，如果 ExecUpdateAct() 报告执行了
				 * 跨分区更新，那么我们就没有其他事情可做——
				 * 更新已被转化为删除和插入，且我们不得执行任何
				 * 通常的后更新任务。
				 */
				if (fc_updateCxt.crossPartUpdate)
				{
					fc_mtstate->mt_merge_updated += 1;
					goto out;
				}

				if (fc_result == TM_Ok && fc_updateCxt.updated)
				{
					fc_ExecUpdateEpilogue(fc_context, &fc_updateCxt, fc_resultRelInfo,
									   fc_tupleid, NULL, fc_newslot);
					fc_mtstate->mt_merge_updated += 1;
				}
				break;

			case CMD_DELETE:
				fc_context->relaction = fc_relaction;
				if (!fc_ExecDeletePrologue(fc_context, fc_resultRelInfo, fc_tupleid,
										NULL, NULL, &fc_result))
				{
					if (fc_result == TM_Ok)
						goto out;	/* "不做任何操作" */
					break;		/* 并发更新/删除 */
				}
				fc_result = fc_ExecDeleteAct(fc_context, fc_resultRelInfo, fc_tupleid, false);
				if (fc_result == TM_Ok)
				{
					fc_ExecDeleteEpilogue(fc_context, fc_resultRelInfo, fc_tupleid, NULL,
									   false);
					fc_mtstate->mt_merge_deleted += 1;
				}
				break;

			case CMD_NOTHING:
				/* 什么都不做总是可以的 */
				fc_result = TM_Ok;
				break;

			default:
				elog(ERROR, "unknown action in MERGE WHEN MATCHED clause");
		}

		switch (fc_result)
		{
			case TM_Ok:
				/* 一切正常；执行最终动作 */
				if (fc_canSetTag && fc_commandType != CMD_NOTHING)
					(fc_estate->es_processed)++;

				break;

			case TM_SelfModified:

				/*
				 * 目标元组已经被当前命令或当前事务中的后续命令更新或删除。
				 * 前一种情况是 SQL 标准明确禁止的，
				 * 它坚持 MERGE 连接条件不应将目标行连接到多个源行。
				 *
				 * 后一种情况发生在元组由 BEFORE 触发器中的命令修改，
				 * 或者由查询中使用的易变函数中的命令修改。
				 * 在这种情况下，我们不应忽略 MERGE 操作，但继续执行
				 * 也是同样不安全的。我们不想丢弃原始
				 * MERGE 操作的同时保留基于它的触发操作；
				 * 允许原始 MERGE 操作而丢弃它触发的更新也没有更好。
				 * 所以抛出错误是唯一安全的方案。
				 */
				if (fc_context->tmfd.cmax != fc_estate->es_output_cid)
					ereport(ERROR,
							(errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
							 errmsg("tuple to be updated or deleted was already modified by an operation triggered by the current command"),
							 errhint("Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.")));

				if (TransactionIdIsCurrentTransactionId(fc_context->tmfd.xmax))
					ereport(ERROR,
							(errcode(ERRCODE_CARDINALITY_VIOLATION),
					/* 翻译者：%s 是一个 SQL 命令名 */
							 errmsg("%s command cannot affect row a second time",
									"MERGE"),
							 errhint("Ensure that not more than one source row matches any one target row.")));

				/* 这不应该发生 */
				elog(ERROR, "attempted to update or delete invisible tuple");
				break;

			case TM_Deleted:
				if (IsolationUsesXactSnapshot())
					ereport(ERROR,
							(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
							 errmsg("could not serialize access due to concurrent delete")));

				/*
				 * 如果元组已经被删除，则返回以让调用者处理
				 * 在未匹配子句下进行处理。
				 */
				fc_no_further_action = false;
				goto out;

			case TM_Updated:
				{
					Relation	fc_resultRelationDesc;
					TupleTableSlot *fc_epqslot,
							   *fc_inputslot;
					LockTupleMode fc_lockmode;

					/*
					 * 目标元组已被其他事务并发更新。使用新版本的
					 * 元组运行 EvalPlanQual()。如果它不返回元组，则我们
					 * 切换到未匹配的动作列表。如果它返回一个元组并且连接条件仍然满足，
					 * 那么我们只需从顶部重新检查匹配的动作，
					 * 并执行第一个符合条件的动作。
					 */
					fc_resultRelationDesc = fc_resultRelInfo->ri_RelationDesc;
					fc_lockmode = ExecUpdateLockMode(fc_estate, fc_resultRelInfo);

					fc_inputslot = EvalPlanQualSlot(fc_epqstate, fc_resultRelationDesc,
												 fc_resultRelInfo->ri_RangeTableIndex);

					fc_result = table_tuple_lock(fc_resultRelationDesc, fc_tupleid,
											  fc_estate->es_snapshot,
											  fc_inputslot, fc_estate->es_output_cid,
											  fc_lockmode, LockWaitBlock,
											  TUPLE_LOCK_FLAG_FIND_LAST_VERSION,
											  &fc_context->tmfd);
					switch (fc_result)
					{
						case TM_Ok:
							fc_epqslot = EvalPlanQual(fc_epqstate,
												   fc_resultRelationDesc,
												   fc_resultRelInfo->ri_RangeTableIndex,
												   fc_inputslot);

							/*
							 * 如果我们没有得到任何元组，或者我们得到的元组具有
							 * NULL ctid，则返回给调用者：这个不再是一个
							 * 匹配元组，因此他们可以重试未匹配的动作。
							 */
							if (TupIsNull(fc_epqslot))
							{
								fc_no_further_action = false;
								goto out;
							}

							(void) ExecGetJunkAttribute(fc_epqslot,
														fc_resultRelInfo->ri_RowIdAttNo,
														&fc_isNull);
							if (fc_isNull)
							{
								fc_no_further_action = false;
								goto out;
							}

							/*
							 * 当一个元组被更新并并发迁移到
							 * 另一个分区时，当前的 MERGE 实现无法跟踪。
							 * 可能有更好的方法来处理这种情况，但
							 * 它需要识别元组移到的关系，并将我们的
							 * 当前 resultRelInfo 设置为该关系。
							 */
							if (ItemPointerIndicatesMovedPartitions(&fc_context->tmfd.ctid))
								ereport(ERROR,
										(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
										 errmsg("tuple to be deleted was already moved to another partition due to concurrent update")));

							/*
							 * 非NULL的ctid意味着我们仍然处理
							 * 匹配的案例。重新启动循环，以便我们
							 * 再次应用所有匹配的规则，以确保
							 * 执行第一个符合条件的匹配动作。
							 *
							 * 更新元组ID为新元组的ID，以便于
							 * 我们在顶部进行重新获取。
							 */
							if (fc_resultRelInfo->ri_needLockTagTuple)
								UnlockTuple(fc_resultRelInfo->ri_RelationDesc,
											&fc_lockedtid,
											InplaceUpdateTupleLock);
							ItemPointerCopy(&fc_context->tmfd.ctid, fc_tupleid);
							goto lmerge_matched;

						case TM_Deleted:

							/*
							 * 元组已经删除；告知调用者运行未
							 * 匹配的动作
							 */
							fc_no_further_action = false;
							goto out;

						case TM_SelfModified:

							/*
							 * 这可以在从另一个会话更新的元组
							 * 的更新链中到达，达到一个已经被
							 * 当前命令更新或删除的元组，或在
							 * 当前事务中后续命令的情况下。与
							 * 上述一样，这总是应该被视为一个错误。
							 */
							if (fc_context->tmfd.cmax != fc_estate->es_output_cid)
								ereport(ERROR,
										(errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
										 errmsg("tuple to be updated or deleted was already modified by an operation triggered by the current command"),
										 errhint("Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.")));

							if (TransactionIdIsCurrentTransactionId(fc_context->tmfd.xmax))
								ereport(ERROR,
										(errcode(ERRCODE_CARDINALITY_VIOLATION),
								/* 翻译者：%s 是一个 SQL 命令名 */
										 errmsg("%s command cannot affect row a second time",
												"MERGE"),
										 errhint("Ensure that not more than one source row matches any one target row.")));

							/* 这不应该发生 */
							elog(ERROR, "attempted to update or delete invisible tuple");
							fc_no_further_action = false;
							goto out;

						default:
							/* 查看 ExecDelete() 中的 table_tuple_lock 调用 */
							elog(ERROR, "unexpected table_tuple_lock status: %u",
								 fc_result);
							fc_no_further_action = false;
							goto out;
					}
				}

			case TM_Invisible:
			case TM_WouldBlock:
			case TM_BeingModified:
				/* 这些不应该发生 */
				elog(ERROR, "unexpected tuple operation result: %d", fc_result);
				break;
		}

		/*
		 * 我们已经激活了一个WHEN子句，所以我们不再向
		 * 进一步搜索。这是必要的行为，而不是优化。
		 */
		break;
	}

	/*
	 * 成功执行一个动作或没有找到符合条件的动作。
	 */
out:
	if (ItemPointerIsValid(&fc_lockedtid))
		UnlockTuple(fc_resultRelInfo->ri_RelationDesc, &fc_lockedtid,
					InplaceUpdateTupleLock);
	return fc_no_further_action;
}

/*
 * 执行第一个符合条件的未匹配动作。
 */
static void fc_ExecMergeNotMatched(ModifyTableContext *fc_context, ResultRelInfo *fc_resultRelInfo,
					bool fc_canSetTag)
{
	ModifyTableState *fc_mtstate = fc_context->mtstate;
	ExprContext *fc_econtext = fc_mtstate->ps.ps_ExprContext;
	List	   *fc_actionStates = NIL;
	ListCell   *fc_l;

	/*
	 * 对于插入操作，根关系的合并动作是可以的，因为
	 * 插入的目标列表和WHEN条件只能引用
	 * 源关系，因此我们处理哪个结果关系
	 * 并无所谓。
	 *
	 * XXX这是否意味着我们可以避免为未匹配
	 * 动作在分区表上创建行动状态的副本？
	 */
	fc_actionStates = fc_resultRelInfo->ri_notMatchedMergeAction;

	/*
	 * 使源元组可用于ExecQual和ExecProject。我们不需要
	 * 目标元组，因为WHEN条件和目标列表不能
	 * 引用目标列。
	 */
	fc_econtext->ecxt_scantuple = NULL;
	fc_econtext->ecxt_innertuple = fc_context->planSlot;
	fc_econtext->ecxt_outertuple = NULL;

	foreach(fc_l, fc_actionStates)
	{
		MergeActionState *fc_action = (MergeActionState *) lfirst(fc_l);
		CmdType		fc_commandType = fc_action->mas_action->commandType;
		TupleTableSlot *fc_newslot;

		/*
		 * 测试条件（如果有的话）。
		 *
		 * 在没有任何条件的情况下，我们执行该操作
		 * 不加条件（无需单独检查，因为如果没有条件评估，ExecQual() 将返回 true）。
		 */
		if (!ExecQual(fc_action->mas_whenqual, fc_econtext))
			continue;

		/* 执行声明的操作 */
		switch (fc_commandType)
		{
			case CMD_INSERT:

				/*
				 * 投影元组。在分区表的情况下，
				 * 投影已经构建为使用根描述符，
				 * 因此我们不需要在这里映射元组。
				 */
				fc_newslot = ExecProject(fc_action->mas_proj);
				fc_context->relaction = fc_action;

				(void) fc_ExecInsert(fc_context, fc_mtstate->rootResultRelInfo, fc_newslot,
								  fc_canSetTag, NULL, NULL);
				fc_mtstate->mt_merge_inserted += 1;
				break;
			case CMD_NOTHING:
				/* 不执行任何操作 */
				break;
			default:
				elog(ERROR, "unknown action in MERGE WHEN NOT MATCHED clause");
		}

		/*
		 * 我们已经激活了一个WHEN子句，所以我们不再向
		 * 进一步搜索。这是必要的行为，而不是优化。
		 */
		break;
	}
}

/*
 * 初始化合并执行的状态。
 */
void ExecInitMerge(ModifyTableState *mtstate, EState *estate)
{
	ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
	ResultRelInfo *rootRelInfo = mtstate->rootResultRelInfo;
	ResultRelInfo *resultRelInfo;
	ExprContext *econtext;
	ListCell   *lc;
	int			i;

	if (node->mergeActionLists == NIL)
		return;

	mtstate->mt_merge_subcommands = 0;

	if (mtstate->ps.ps_ExprContext == NULL)
		ExecAssignExprContext(estate, &mtstate->ps);
	econtext = mtstate->ps.ps_ExprContext;

	/*
	 * 为mergeActionList中的每个动作创建一个MergeActionState，并
	 * 将其添加到匹配动作或未匹配动作的列表中。
	 *
	 * ExecInitPartitionInfo()中出现类似逻辑，因此如果要
	 * 更改此处的任何内容，请在那里也进行相应更改。
	 */
	i = 0;
	foreach(lc, node->mergeActionLists)
	{
		List	   *mergeActionList = lfirst(lc);
		TupleDesc	relationDesc;
		ListCell   *l;

		resultRelInfo = mtstate->resultRelInfo + i;
		i++;
		relationDesc = RelationGetDescr(resultRelInfo->ri_RelationDesc);

		/* 初始化从该关系提取的MERGE 插槽 */
		if (unlikely(!resultRelInfo->ri_projectNewInfoValid))
			ExecInitMergeTupleSlots(mtstate, resultRelInfo);

		foreach(l, mergeActionList)
		{
			MergeAction *action = (MergeAction *) lfirst(l);
			MergeActionState *action_state;
			TupleTableSlot *tgtslot;
			TupleDesc	tgtdesc;
			List	  **list;

			/*
			 * 为该关系构建动作合并状态。  (对于分区，
			 * 在ExecInitPartitionInfo中存在等效代码。)
			 */
			action_state = makeNode(MergeActionState);
			action_state->mas_action = action;
			action_state->mas_whenqual = ExecInitQual((List *) action->qual,
													  &mtstate->ps);

			/*
			 * 我们创建两个列表 - 一个用于匹配的
			 * 动作，一个用于未匹配的动作 - 并将
			 * MergeActionState放入适当的列表中。
			 */
			if (action_state->mas_action->matched)
				list = &resultRelInfo->ri_matchedMergeAction;
			else
				list = &resultRelInfo->ri_notMatchedMergeAction;
			*list = lappend(*list, action_state);

			switch (action->commandType)
			{
				case CMD_INSERT:
					fc_ExecCheckPlanOutput(rootRelInfo->ri_RelationDesc,
										action->targetList);

					/*
					 * 如果MERGE目标是一个分区表，任何插入
					 * 动作必须通过它进行，而不是通过子
					 * 关系进行。如果尚未完成，初始化路由结构
					 * 和根表的“新”元组插槽。我们为所有关系
					 * 准备的投影使用根关系描述符，并以计划的根
					 * 插槽为目标。  (这一点与我们
					 * 检查计划输出以匹配根关系一致。)
					 */
					if (rootRelInfo->ri_RelationDesc->rd_rel->relkind ==
						RELKIND_PARTITIONED_TABLE)
					{
						if (mtstate->mt_partition_tuple_routing == NULL)
						{
							/*
							 * 如果尚未完成，初始化路由的计划状态。
							 *
							 * 请注意，该插槽被管理为属于
							 * ModifyTableState的独立插槽，
							 * 因此我们为第二个参数传递NULL。
							 */
							mtstate->mt_root_tuple_slot =
								table_slot_create(rootRelInfo->ri_RelationDesc,
												  NULL);
							mtstate->mt_partition_tuple_routing =
								ExecSetupPartitionTupleRouting(estate,
															   rootRelInfo->ri_RelationDesc);
						}
						tgtslot = mtstate->mt_root_tuple_slot;
						tgtdesc = RelationGetDescr(rootRelInfo->ri_RelationDesc);
					}
					else
					{
						/* 不分区？使用标准关系和插槽 */
						tgtslot = resultRelInfo->ri_newTupleSlot;
						tgtdesc = RelationGetDescr(resultRelInfo->ri_RelationDesc);
					}

					action_state->mas_proj =
						ExecBuildProjectionInfo(action->targetList, econtext,
												tgtslot,
												&mtstate->ps,
												tgtdesc);

					mtstate->mt_merge_subcommands |= MERGE_INSERT;
					break;
				case CMD_UPDATE:
					action_state->mas_proj =
						ExecBuildUpdateProjection(action->targetList,
												  true,
												  action->updateColnos,
												  relationDesc,
												  econtext,
												  resultRelInfo->ri_newTupleSlot,
												  &mtstate->ps);
					mtstate->mt_merge_subcommands |= MERGE_UPDATE;
					break;
				case CMD_DELETE:
					mtstate->mt_merge_subcommands |= MERGE_DELETE;
					break;
				case CMD_NOTHING:
					break;
				default:
					elog(ERROR, "unknown operation");
					break;
			}
		}
	}
}

/*
 * 初始化任何MERGE动作中的ResultRelInfo中的元组插槽。
 *
 * 尽管这里并没有初始化投影，但我们标记'projectNewInfoValid'。
 */
void ExecInitMergeTupleSlots(ModifyTableState *fc_mtstate,
						ResultRelInfo *fc_resultRelInfo)
{
	EState	   *fc_estate = fc_mtstate->ps.state;

	Assert(!fc_resultRelInfo->ri_projectNewInfoValid);

	fc_resultRelInfo->ri_oldTupleSlot =
		table_slot_create(fc_resultRelInfo->ri_RelationDesc,
						  &fc_estate->es_tupleTable);
	fc_resultRelInfo->ri_newTupleSlot =
		table_slot_create(fc_resultRelInfo->ri_RelationDesc,
						  &fc_estate->es_tupleTable);
	fc_resultRelInfo->ri_projectNewInfoValid = true;
}

/*
 * 处理每个语句之前的触发器
 */
static void fc_fireBSTriggers(ModifyTableState *fc_node)
{
	ModifyTable *fc_plan = (ModifyTable *) fc_node->ps.plan;
	ResultRelInfo *fc_resultRelInfo = fc_node->rootResultRelInfo;

	switch (fc_node->operation)
	{
		case CMD_INSERT:
			ExecBSInsertTriggers(fc_node->ps.state, fc_resultRelInfo);
			if (fc_plan->onConflictAction == ONCONFLICT_UPDATE)
				ExecBSUpdateTriggers(fc_node->ps.state,
									 fc_resultRelInfo);
			break;
		case CMD_UPDATE:
			ExecBSUpdateTriggers(fc_node->ps.state, fc_resultRelInfo);
			break;
		case CMD_DELETE:
			ExecBSDeleteTriggers(fc_node->ps.state, fc_resultRelInfo);
			break;
		case CMD_MERGE:
			if (fc_node->mt_merge_subcommands & MERGE_INSERT)
				ExecBSInsertTriggers(fc_node->ps.state, fc_resultRelInfo);
			if (fc_node->mt_merge_subcommands & MERGE_UPDATE)
				ExecBSUpdateTriggers(fc_node->ps.state, fc_resultRelInfo);
			if (fc_node->mt_merge_subcommands & MERGE_DELETE)
				ExecBSDeleteTriggers(fc_node->ps.state, fc_resultRelInfo);
			break;
		default:
			elog(ERROR, "unknown operation");
			break;
	}
}

/*
 * 处理每个语句之后的触发器
 */
static void fc_fireASTriggers(ModifyTableState *fc_node)
{
	ModifyTable *fc_plan = (ModifyTable *) fc_node->ps.plan;
	ResultRelInfo *fc_resultRelInfo = fc_node->rootResultRelInfo;

	switch (fc_node->operation)
	{
		case CMD_INSERT:
			if (fc_plan->onConflictAction == ONCONFLICT_UPDATE)
				ExecASUpdateTriggers(fc_node->ps.state,
									 fc_resultRelInfo,
									 fc_node->mt_oc_transition_capture);
			ExecASInsertTriggers(fc_node->ps.state, fc_resultRelInfo,
								 fc_node->mt_transition_capture);
			break;
		case CMD_UPDATE:
			ExecASUpdateTriggers(fc_node->ps.state, fc_resultRelInfo,
								 fc_node->mt_transition_capture);
			break;
		case CMD_DELETE:
			ExecASDeleteTriggers(fc_node->ps.state, fc_resultRelInfo,
								 fc_node->mt_transition_capture);
			break;
		case CMD_MERGE:
			if (fc_node->mt_merge_subcommands & MERGE_DELETE)
				ExecASDeleteTriggers(fc_node->ps.state, fc_resultRelInfo,
									 fc_node->mt_transition_capture);
			if (fc_node->mt_merge_subcommands & MERGE_UPDATE)
				ExecASUpdateTriggers(fc_node->ps.state, fc_resultRelInfo,
									 fc_node->mt_transition_capture);
			if (fc_node->mt_merge_subcommands & MERGE_INSERT)
				ExecASInsertTriggers(fc_node->ps.state, fc_resultRelInfo,
									 fc_node->mt_transition_capture);
			break;
		default:
			elog(ERROR, "unknown operation");
			break;
	}
}

/*
 * 设置所需的状态，以收集用于后续触发器的过渡元组。
 */
static void fc_ExecSetupTransitionCaptureState(ModifyTableState *fc_mtstate, EState *fc_estate)
{
	ModifyTable *fc_plan = (ModifyTable *) fc_mtstate->ps.plan;
	ResultRelInfo *fc_targetRelInfo = fc_mtstate->rootResultRelInfo;

	/* 检查直接针对的关系上的过渡表。 */
	fc_mtstate->mt_transition_capture =
		MakeTransitionCaptureState(fc_targetRelInfo->ri_TrigDesc,
								   RelationGetRelid(fc_targetRelInfo->ri_RelationDesc),
								   fc_mtstate->operation);
	if (fc_plan->operation == CMD_INSERT &&
		fc_plan->onConflictAction == ONCONFLICT_UPDATE)
		fc_mtstate->mt_oc_transition_capture =
			MakeTransitionCaptureState(fc_targetRelInfo->ri_TrigDesc,
									   RelationGetRelid(fc_targetRelInfo->ri_RelationDesc),
									   CMD_UPDATE);
}

/*
 * ExecPrepareTupleRouting --- 为路由一个元组做准备
 *
 * 确定要插入的元组在槽中的分区，
 * 并返回其ResultRelInfo在*partRelInfo中。
 * 返回值是一个持有分区行类型的元组槽。
 *
 * 这还根据所选分区设置mtstate中的过渡表信息。
 */
static TupleTableSlot * fc_ExecPrepareTupleRouting(ModifyTableState *fc_mtstate,
						EState *fc_estate,
						PartitionTupleRouting *fc_proute,
						ResultRelInfo *fc_targetRelInfo,
						TupleTableSlot *fc_slot,
						ResultRelInfo **fc_partRelInfo)
{
	ResultRelInfo *fc_partrel;
	TupleConversionMap *fc_map;

	/*
	 * 查找目标分区的ResultRelInfo。如果ExecFindPartition没有
	 * 为'slot'中的元组找到有效分区，则会引发错误。
	 * 如果找到的分区不是INSERT的有效目标，也会引发错误。
	 * 这是必需的，因为一个分区表UPDATE到另一个分区会
	 * 变成DELETE+INSERT。
	 */
	fc_partrel = ExecFindPartition(fc_mtstate, fc_targetRelInfo, fc_proute, fc_slot, fc_estate);

	
/*
	 * 如果我们正在捕获转换元组，我们可能需要将从
	 * 分区行类型转换为根分区表的行类型。但是如果
	 * 分区上没有可以更改元组的BEFORE触发器，我们
	 * 可以只是记住原始未转换元组，以避免不必要的
	 * 往返转换。
	 */
	if (fc_mtstate->mt_transition_capture != NULL)
	{
		bool		fc_has_before_insert_row_trig;

		fc_has_before_insert_row_trig = (fc_partrel->ri_TrigDesc &&
									  fc_partrel->ri_TrigDesc->trig_insert_before_row);

		fc_mtstate->mt_transition_capture->tcs_original_insert_tuple =
			!fc_has_before_insert_row_trig ? fc_slot : NULL;
	}

	/*
	 * 如果需要，转换元组。
	 */
	fc_map = fc_partrel->ri_RootToPartitionMap;
	if (fc_map != NULL)
	{
		TupleTableSlot *fc_new_slot = fc_partrel->ri_PartitionTupleSlot;

		fc_slot = execute_attr_map_slot(fc_map->attrMap, fc_slot, fc_new_slot);
	}

	*fc_partRelInfo = fc_partrel;
	return fc_slot;
}

/* ----------------------------------------------------------------
 *	   ExecModifyTable
 *
 *		根据需要执行表修改，并在需要时返回RETURNING结果。
 * ----------------------------------------------------------------
 */
static TupleTableSlot * fc_ExecModifyTable(PlanState *fc_pstate)
{
	ModifyTableState *fc_node = castNode(ModifyTableState, fc_pstate);
	ModifyTableContext fc_context;
	EState	   *fc_estate = fc_node->ps.state;
	CmdType		fc_operation = fc_node->operation;
	ResultRelInfo *fc_resultRelInfo;
	PlanState  *fc_subplanstate;
	TupleTableSlot *fc_slot;
	TupleTableSlot *fc_oldSlot;
	ItemPointerData fc_tuple_ctid;
	HeapTupleData fc_oldtupdata;
	HeapTuple	fc_oldtuple;
	ItemPointer fc_tupleid;
	bool		fc_tuplock;

	CHECK_FOR_INTERRUPTS();

	/*
	 * 这个在 EvalPlanQual 期间不应该被调用；我们应该传递一个
	 * 子计划树给 EvalPlanQual。使用运行时测试而不仅仅是
	 * Assert，因为这个条件在测试中容易被遗漏。 （注意：
	 * 尽管 ModifyTable 不应该在 EvalPlanQual
	 * 操作中执行，但我们必须允许它在 CTE 子计划中被初始化和关闭。
	 * 因此，这个测试必须在这里，而不是在
	 * ExecInitModifyTable 中。）
	 */
	if (fc_estate->es_epq_active != NULL)
		elog(ERROR, "ModifyTable should not be called during EvalPlanQual");

	/*
	 * 如果我们已经完成处理，就不要尝试更多的操作。我们需要
	 * 这个测试，因为 ExecPostprocessPlan 可能会多次调用我们，
	 * 而我们的子计划节点不一定能够承受额外的调用。
	 */
	if (fc_node->mt_done)
		return NULL;

	/*
	 * 在第一次调用时，触发执行 BEFORE STATEMENT 触发器，然后再继续。
	 */
	if (fc_node->fireBSTriggers)
	{
		fc_fireBSTriggers(fc_node);
		fc_node->fireBSTriggers = false;
	}

	/* 预加载局部变量 */
	fc_resultRelInfo = fc_node->resultRelInfo + fc_node->mt_lastResultIndex;
	fc_subplanstate = outerPlanState(fc_node);

	/* 设置全局上下文 */
	fc_context.mtstate = fc_node;
	fc_context.epqstate = &fc_node->mt_epqstate;
	fc_context.estate = fc_estate;

	/*
	 * 从子计划中获取行，并对每行执行所需的表修改。
	 */
	for (;;)
	{
		/*
		 * 重置每个输出元组的 exprcontext。这是必要的，因为
		 * 触发器期望使用该上下文作为工作区。然而，在计划的顶层以下
		 * 这样做有点难看。我们可能需要稍后重新考虑这个问题。
		 */
		ResetPerTupleExprContext(fc_estate);

		/*
		 * 重置用于处理冲突和返回子句的每个元组内存上下文，以释放
		 * 在上一个周期中分配的任何表达式评估存储。
		 */
		if (fc_pstate->ps_ExprContext)
			ResetExprContext(fc_pstate->ps_ExprContext);

		fc_context.planSlot = ExecProcNode(fc_subplanstate);

		/* 没有更多元组要处理了吗？ */
		if (TupIsNull(fc_context.planSlot))
			break;

		/*
		 * 当存在多个结果关系时，每个元组包含一个垃圾列，给出
		 * 它来自的关系的 OID。提取它并选择正确的结果关系。
		 */
		if (AttributeNumberIsValid(fc_node->mt_resultOidAttno))
		{
			Datum		fc_datum;
			bool		fc_isNull;
			Oid			fc_resultoid;

			fc_datum = ExecGetJunkAttribute(fc_context.planSlot, fc_node->mt_resultOidAttno,
										 &fc_isNull);
			if (fc_isNull)
			{
				/*
				 * 对于 MERGE 以外的命令，任何具有无效 Oid
				 * 的 tableoid 都是错误。对于 MERGE，我们可能需要处理
				 * 它们作为 WHEN NOT MATCHED 子句（如果有的话），所以这样做。
				 *
				 * 注意，我们使用节点的顶层 resultRelInfo，而不是任何
				 * 特定分区的。
				 */
				if (fc_operation == CMD_MERGE)
				{
					EvalPlanQualSetSlot(&fc_node->mt_epqstate, fc_context.planSlot);

					fc_ExecMerge(&fc_context, fc_node->resultRelInfo, NULL, fc_node->canSetTag);
					continue;	/* 暂时不支持 RETURNING */
				}

				elog(ERROR, "tableoid is NULL");
			}
			fc_resultoid = DatumGetObjectId(fc_datum);

			/* 如果这次与上次不同，我们需要定位关系 */
			if (fc_resultoid != fc_node->mt_lastResultOid)
				fc_resultRelInfo = ExecLookupResultRelByOid(fc_node, fc_resultoid,
														 false, true);
		}

		/*
		 * 如果 resultRelInfo->ri_usesFdwDirectModify 为真，
		 * 我们在这里需要做的只是计算 RETURNING 表达式。
		 */
		if (fc_resultRelInfo->ri_usesFdwDirectModify)
		{
			Assert(fc_resultRelInfo->ri_projectReturning);

			/*
			 * 一个包含实际插入、更新或删除的数据的扫描槽
			 * 已通过 IterateDirectModify 提供给 ExecProcessReturning，
			 * 所以在这里不需要提供它。
			 */
			fc_slot = fc_ExecProcessReturning(fc_resultRelInfo, NULL, fc_context.planSlot);

			return fc_slot;
		}

		EvalPlanQualSetSlot(&fc_node->mt_epqstate, fc_context.planSlot);
		fc_slot = fc_context.planSlot;

		fc_tupleid = NULL;
		fc_oldtuple = NULL;

		/*
		 * 对于 UPDATE/DELETE/MERGE，获取要更新/删除/合并的元组的行身份信息。
		 * 对于堆关系，这是一个 TID；否则，我们可能会有一个携带旧元组的
		 * wholerow 垃圾属性。保持这与
		 * ExecInitModifyTable 中设置的 ri_RowIdAttNo 步骤一致。
		 */
		if (fc_operation == CMD_UPDATE || fc_operation == CMD_DELETE ||
			fc_operation == CMD_MERGE)
		{
			char		fc_relkind;
			Datum		fc_datum;
			bool		fc_isNull;

			fc_relkind = fc_resultRelInfo->ri_RelationDesc->rd_rel->relkind;
			if (fc_relkind == RELKIND_RELATION ||
				fc_relkind == RELKIND_MATVIEW ||
				fc_relkind == RELKIND_PARTITIONED_TABLE)
			{
				/* ri_RowIdAttNo 指的是 ctid 属性 */
				Assert(AttributeNumberIsValid(fc_resultRelInfo->ri_RowIdAttNo));
				fc_datum = ExecGetJunkAttribute(fc_slot,
											 fc_resultRelInfo->ri_RowIdAttNo,
											 &fc_isNull);

				/*
				 * 对于 MERGE 以外的命令，任何具有空行标识符的元组都是错误。
				 * 对于 MERGE，我们可能需要处理它们作为 WHEN NOT MATCHED 子句（如果有的话），所以这样做。
				 *
				 * 注意，我们使用节点的顶层 resultRelInfo，而不是任何
				 * 特定分区的。
				 */
				if (fc_isNull)
				{
					if (fc_operation == CMD_MERGE)
					{
						EvalPlanQualSetSlot(&fc_node->mt_epqstate, fc_context.planSlot);

						fc_ExecMerge(&fc_context, fc_node->resultRelInfo, NULL, fc_node->canSetTag);
						continue;	/* 暂时不支持 RETURNING */
					}

					elog(ERROR, "ctid is NULL");
				}

				fc_tupleid = (ItemPointer) DatumGetPointer(fc_datum);
				fc_tuple_ctid = *fc_tupleid;	/* 确保我们不会释放 ctid!! */
				fc_tupleid = &fc_tuple_ctid;
			}

			/*
			 * 使用 wholerow 属性（在可用时）来重建
			 * 旧关系元组。旧元组服务于两个目的之一或两个目的：
			 * 1）它作为行触发器的 OLD 元组，2）它为
			 * 更新的 NEW 元组提供任何未更改列的值，因为子计划
			 * 不会生成目标表的所有列。
			 *
			 * 注意，wholerow 属性不携带系统列，
			 * 因此外部表触发器不会看到这些，尽管我们
			 * 在这里知道足够的信息来设置 t_tableOid。与此完全分开的是，
			 * FDW 可能会获取自己的垃圾属性来识别该行。
			 *
			 * 其他相关的 relkinds，目前限于具有
			 * INSTEAD OF 触发器的视图，始终具有一个 wholerow 属性。
			 */
			else if (AttributeNumberIsValid(fc_resultRelInfo->ri_RowIdAttNo))
			{
				fc_datum = ExecGetJunkAttribute(fc_slot,
											 fc_resultRelInfo->ri_RowIdAttNo,
											 &fc_isNull);
				/* 不应该得到空结果…… */
				if (fc_isNull)
					elog(ERROR, "wholerow is NULL");

				fc_oldtupdata.t_data = DatumGetHeapTupleHeader(fc_datum);
				fc_oldtupdata.t_len =
					HeapTupleHeaderGetDatumLength(fc_oldtupdata.t_data);
				ItemPointerSetInvalid(&(fc_oldtupdata.t_self));
				/* 历史上，视图触发器看到无效的 t_tableOid。 */
				fc_oldtupdata.t_tableOid =
					(fc_relkind == RELKIND_VIEW) ? InvalidOid :
					RelationGetRelid(fc_resultRelInfo->ri_RelationDesc);

				fc_oldtuple = &fc_oldtupdata;
			}
			else
			{
				/* 仅允许外部表省略行 ID 属性 */
				Assert(fc_relkind == RELKIND_FOREIGN_TABLE);
			}
		}

		switch (fc_operation)
		{
			case CMD_INSERT:
				/* 如果这是该表的第一次，初始化投影信息 */
				if (unlikely(!fc_resultRelInfo->ri_projectNewInfoValid))
					fc_ExecInitInsertProjection(fc_node, fc_resultRelInfo);
				fc_slot = fc_ExecGetInsertNewTuple(fc_resultRelInfo, fc_context.planSlot);
				fc_slot = fc_ExecInsert(&fc_context, fc_resultRelInfo, fc_slot,
								  fc_node->canSetTag, NULL, NULL);
				break;

			case CMD_UPDATE:
				fc_tuplock = false;

				/* 如果这是该表的第一次，初始化投影信息 */
				if (unlikely(!fc_resultRelInfo->ri_projectNewInfoValid))
					fc_ExecInitUpdateProjection(fc_node, fc_resultRelInfo);

				/*
				 * 通过将计划的输出元组与
				 * 旧元组结合来创建新元组。
				 */
				fc_oldSlot = fc_resultRelInfo->ri_oldTupleSlot;
				if (fc_oldtuple != NULL)
				{
					Assert(!fc_resultRelInfo->ri_needLockTagTuple);
					/* 将 wholerow 垃圾属性用作旧元组。 */
					ExecForceStoreHeapTuple(fc_oldtuple, fc_oldSlot, false);
				}
				else
				{
					/* 获取旧元组的最新版本。 */
					Relation	fc_relation = fc_resultRelInfo->ri_RelationDesc;

					if (fc_resultRelInfo->ri_needLockTagTuple)
					{
						LockTuple(fc_relation, fc_tupleid, InplaceUpdateTupleLock);
						fc_tuplock = true;
					}
					if (!table_tuple_fetch_row_version(fc_relation, fc_tupleid,
													   SnapshotAny,
													   fc_oldSlot))
						elog(ERROR, "failed to fetch tuple being updated");
				}
				fc_slot = ExecGetUpdateNewTuple(fc_resultRelInfo, fc_context.planSlot,
											 fc_oldSlot);
				fc_context.relaction = NULL;

				/* 现在应用更新。 */
				fc_slot = fc_ExecUpdate(&fc_context, fc_resultRelInfo, fc_tupleid, fc_oldtuple,
								  fc_slot, fc_node->canSetTag);
				if (fc_tuplock)
					UnlockTuple(fc_resultRelInfo->ri_RelationDesc, fc_tupleid,
								InplaceUpdateTupleLock);
				break;

			case CMD_DELETE:
				fc_slot = fc_ExecDelete(&fc_context, fc_resultRelInfo, fc_tupleid, fc_oldtuple,
								  true, false, fc_node->canSetTag, NULL, NULL, NULL);
				break;

			case CMD_MERGE:
				fc_slot = fc_ExecMerge(&fc_context, fc_resultRelInfo, fc_tupleid, fc_node->canSetTag);
				break;

			default:
				elog(ERROR, "unknown operation");
				break;
		}

		/*
		 * 如果我们得到了 RETURNING 结果，将其返回给调用者。我们将在下一次调用时继续工作。
		 */
		if (fc_slot)
			return fc_slot;
	}

	/*
	 * 插入剩余的元组以进行批量插入。
	 */
	if (fc_estate->es_insert_pending_result_relations != NIL)
		fc_ExecPendingInserts(fc_estate);

	/*
	 * 我们完成了，但在退出之前先触发 AFTER STATEMENT 触发器。
	 */
	fc_fireASTriggers(fc_node);

	fc_node->mt_done = true;

	return NULL;
}


ResultRelInfo * ExecLookupResultRelByOid(ModifyTableState *fc_node, Oid fc_resultoid,
						 bool fc_missing_ok, bool update_cache)
{
	if (fc_node->mt_resultOidHash)
	{
		/* 使用预构建的哈希表查找目标关系 */
		MTTargetRelLookup *fc_mtlookup;

		fc_mtlookup = (MTTargetRelLookup *)
			hash_search(fc_node->mt_resultOidHash, &fc_resultoid, HASH_FIND, NULL);
		if (fc_mtlookup)
		{
			if (update_cache)
			{
				fc_node->mt_lastResultOid = fc_resultoid;
				fc_node->mt_lastResultIndex = fc_mtlookup->relationIndex;
			}
			return fc_node->resultRelInfo + fc_mtlookup->relationIndex;
		}
	}
	else
	{
		/* 目标关系较少时，只需搜索ResultRelInfo数组 */
		for (int fc_ndx = 0; fc_ndx < fc_node->mt_nrels; fc_ndx++)
		{
			ResultRelInfo *fc_rInfo = fc_node->resultRelInfo + fc_ndx;

			if (RelationGetRelid(fc_rInfo->ri_RelationDesc) == fc_resultoid)
			{
				if (update_cache)
				{
					fc_node->mt_lastResultOid = fc_resultoid;
					fc_node->mt_lastResultIndex = fc_ndx;
				}
				return fc_rInfo;
			}
		}
	}

	if (!fc_missing_ok)
		elog(ERROR, "incorrect result relation OID %u", fc_resultoid);
	return NULL;
}

/* ----------------------------------------------------------------
 *        ExecInitModifyTable
 * ----------------------------------------------------------------
 */
ModifyTableState *
ExecInitModifyTable(ModifyTable *fc_node, EState *fc_estate, int fc_eflags)
{
	ModifyTableState *fc_mtstate;
	Plan	   *fc_subplan = outerPlan(fc_node);
	CmdType		fc_operation = fc_node->operation;
	int			fc_nrels = list_length(fc_node->resultRelations);
	ResultRelInfo *fc_resultRelInfo;
	List	   *fc_arowmarks;
	ListCell   *fc_l;
	int			fc_i;
	Relation	fc_rel;

	/* 检查不支持的标志 */
	Assert(!(fc_eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));

	/*
	 * 创建状态结构
	 */
	fc_mtstate = makeNode(ModifyTableState);
	fc_mtstate->ps.plan = (Plan *) fc_node;
	fc_mtstate->ps.state = fc_estate;
	fc_mtstate->ps.ExecProcNode = fc_ExecModifyTable;

	fc_mtstate->operation = fc_operation;
	fc_mtstate->canSetTag = fc_node->canSetTag;
	fc_mtstate->mt_done = false;

	fc_mtstate->mt_nrels = fc_nrels;
	fc_mtstate->resultRelInfo = (ResultRelInfo *)
		palloc(fc_nrels * sizeof(ResultRelInfo));

	fc_mtstate->mt_merge_inserted = 0;
	fc_mtstate->mt_merge_updated = 0;
	fc_mtstate->mt_merge_deleted = 0;

	/* ----------
	 * 解析目标关系。这与以下内容相同：
	 *
	 * - 我们将触发FOR STATEMENT触发器的关系，
	 * - 所有捕获的过渡元组必须转换为的元组格式的关系，以及
	 * - 用于元组路由的根分区表。
	 *
	 * 如果它是一个分区或继承表，则根分区或appendrel RTE不会在计划的其他地方出现，其RT索引在node->rootRelation中显式给出。否则，目标关系是node->resultRelations列表中的唯一关系。
	 * ----------
	 */
	if (fc_node->rootRelation > 0)
	{
		fc_mtstate->rootResultRelInfo = makeNode(ResultRelInfo);
		ExecInitResultRelation(fc_estate, fc_mtstate->rootResultRelInfo,
							   fc_node->rootRelation);
	}
	else
	{
		Assert(list_length(fc_node->resultRelations) == 1);
		fc_mtstate->rootResultRelInfo = fc_mtstate->resultRelInfo;
		ExecInitResultRelation(fc_estate, fc_mtstate->resultRelInfo,
							   linitial_int(fc_node->resultRelations));
	}

	/* 现在暂时用虚拟子计划数据设置epqstate */
	EvalPlanQualInitExt(&fc_mtstate->mt_epqstate, fc_estate, NULL, NIL,
						fc_node->epqParam, fc_node->resultRelations);
	fc_mtstate->fireBSTriggers = true;

	/*
	 * 构建用于收集过渡元组的状态。这需要有一个有效的触发器查询上下文，因此在仅说明模式下跳过它。
	 */
	if (!(fc_eflags & EXEC_FLAG_EXPLAIN_ONLY))
		fc_ExecSetupTransitionCaptureState(fc_mtstate, fc_estate);

	/*
	 * 打开所有结果关系并初始化ResultRelInfo结构。
	 * （但如果是数组的一部分，则根关系已在上面初始化。）
	 * 我们必须在初始化子计划之前执行此操作，因为直接修改
	 * 的FDW期望其ResultRelInfos可用。
	 */
	fc_resultRelInfo = fc_mtstate->resultRelInfo;
	fc_i = 0;
	foreach(fc_l, fc_node->resultRelations)
	{
		Index		fc_resultRelation = lfirst_int(fc_l);

		if (fc_resultRelInfo != fc_mtstate->rootResultRelInfo)
		{
			ExecInitResultRelation(fc_estate, fc_resultRelInfo, fc_resultRelation);

			/*
			 * 对于子结果关系，存储根结果关系指针。为了方便希望查看查询原始目标关系但没有mtstate的地方，我们这样做。
			 */
			fc_resultRelInfo->ri_RootResultRelInfo = fc_mtstate->rootResultRelInfo;
		}

		/* 初始化usesFdwDirectModify标志 */
		fc_resultRelInfo->ri_usesFdwDirectModify =
			bms_is_member(fc_i, fc_node->fdwDirectModifyPlans);

		/*
		 * 验证结果关系是当前操作的有效目标
 */
		CheckValidResultRel(fc_resultRelInfo, fc_operation);

		fc_resultRelInfo++;
		fc_i++;
	}

	/*
	 * 现在我们可以初始化子计划了。
	 */
	outerPlanState(fc_mtstate) = ExecInitNode(fc_subplan, fc_estate, fc_eflags);

	/*
	 * 进行额外的每结果关系初始化。
	 */
	for (fc_i = 0; fc_i < fc_nrels; fc_i++)
	{
		fc_resultRelInfo = &fc_mtstate->resultRelInfo[fc_i];

		/* 让FDWs为外部表结果关系进行初始化 */
		if (!fc_resultRelInfo->ri_usesFdwDirectModify &&
			fc_resultRelInfo->ri_FdwRoutine != NULL &&
			fc_resultRelInfo->ri_FdwRoutine->BeginForeignModify != NULL)
		{
			List	   *fc_fdw_private = (List *) list_nth(fc_node->fdwPrivLists, fc_i);

			fc_resultRelInfo->ri_FdwRoutine->BeginForeignModify(fc_mtstate,
															 fc_resultRelInfo,
															 fc_fdw_private,
															 fc_i,
															 fc_eflags);
		}

		/*
		 * 对于UPDATE/DELETE/MERGE，现在找到适当的杂项属性，
		 * 根据relkind为“ctid”或“wholerow”属性。对于外部表，
		 * FDW可能已创建额外的杂项属性，但这些不在我们关注之列。
		 */
		if (fc_operation == CMD_UPDATE || fc_operation == CMD_DELETE ||
			fc_operation == CMD_MERGE)
		{
			char		fc_relkind;

			fc_relkind = fc_resultRelInfo->ri_RelationDesc->rd_rel->relkind;
			if (fc_relkind == RELKIND_RELATION ||
				fc_relkind == RELKIND_MATVIEW ||
				fc_relkind == RELKIND_PARTITIONED_TABLE)
			{
				fc_resultRelInfo->ri_RowIdAttNo =
					ExecFindJunkAttributeInTlist(fc_subplan->targetlist, "ctid");
				if (!AttributeNumberIsValid(fc_resultRelInfo->ri_RowIdAttNo))
					elog(ERROR, "could not find junk ctid column");
			}
			else if (fc_relkind == RELKIND_FOREIGN_TABLE)
			{
				/*
				 * 目前我们不支持与外部表的MERGE。（这是有问题的，因为实现使用CTID。）
				 */
				Assert(fc_operation != CMD_MERGE);

				/*
				 * 当有行级触发器时，应该有一个
				 * wholerow属性。我们还要求它在UPDATE和MERGE中存在，以便我们可以获取未更改列的值。
				 */
				fc_resultRelInfo->ri_RowIdAttNo =
					ExecFindJunkAttributeInTlist(fc_subplan->targetlist,
												 "wholerow");
				if ((fc_mtstate->operation == CMD_UPDATE || fc_mtstate->operation == CMD_MERGE) &&
					!AttributeNumberIsValid(fc_resultRelInfo->ri_RowIdAttNo))
					elog(ERROR, "could not find junk wholerow column");
			}
			else
			{
				/* 不支持MERGE */
				Assert(fc_operation != CMD_MERGE);
				/* 其他有效目标关系类型必须提供wholerow */
				fc_resultRelInfo->ri_RowIdAttNo =
					ExecFindJunkAttributeInTlist(fc_subplan->targetlist,
												 "wholerow");
				if (!AttributeNumberIsValid(fc_resultRelInfo->ri_RowIdAttNo))
					elog(ERROR, "could not find junk wholerow column");
			}
		}

		/*
		 * 对于INSERT/UPDATE/MERGE，准备评估任何生成的列。
		 * 我们现在必须这样做，即使我们从未插入或更新任何行，也要覆盖MERGE执行一些UPDATE操作并随后执行一些INSERT的情况。因此，我们需要ri_GeneratedExprs来覆盖所有生成的列，因此我们现在强制它。（仅对operation == CMD_MERGE这样做可能就足够，但我们将避免在后支中更改数据结构定义。）
		 */
		if (fc_operation == CMD_INSERT || fc_operation == CMD_UPDATE || fc_operation == CMD_MERGE)
			ExecInitStoredGenerated(fc_resultRelInfo, fc_estate, fc_operation);
	}

	/*
	 * 如果这是一个继承的更新/删除/合并，则子计划的目标列表中将存在一个名为“tableoid”的杂项属性。
	 * 它将用于标识要更新/删除/合并的给定元组的结果关系。
	 */
	fc_mtstate->mt_resultOidAttno =
		ExecFindJunkAttributeInTlist(fc_subplan->targetlist, "tableoid");
	Assert(AttributeNumberIsValid(fc_mtstate->mt_resultOidAttno) || fc_nrels == 1);
	fc_mtstate->mt_lastResultOid = InvalidOid; /* 强制在第一个元组时查找 */
	fc_mtstate->mt_lastResultIndex = 0;	/* 如果没有此类属性，则必须为零 */

	/* 获取根目标关系 */
	fc_rel = fc_mtstate->rootResultRelInfo->ri_RelationDesc;

	/*
	 * 如果这是一个分区INSERT，则构建用于元组路由的状态。更新
	 * 或MERGE也可能需要此操作，但仅在它确实在分区之间移动元组的情况下；在这种情况下，由ExecCrossPartitionUpdate完成设置。
	 */
	if (fc_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
		fc_operation == CMD_INSERT)
		fc_mtstate->mt_partition_tuple_routing =
			ExecSetupPartitionTupleRouting(fc_estate, fc_rel);

	/*
	 * 如果需要，初始化任何WITH CHECK OPTION约束。
	 */
	fc_resultRelInfo = fc_mtstate->resultRelInfo;
	foreach(fc_l, fc_node->withCheckOptionLists)
	{
		List	   *fc_wcoList = (List *) lfirst(fc_l);
		List	   *fc_wcoExprs = NIL;
		ListCell   *fc_ll;

		foreach(fc_ll, fc_wcoList)
		{
			WithCheckOption *fc_wco = (WithCheckOption *) lfirst(fc_ll);
			ExprState  *fc_wcoExpr = ExecInitQual((List *) fc_wco->qual,
											   &fc_mtstate->ps);

			fc_wcoExprs = lappend(fc_wcoExprs, fc_wcoExpr);
		}

		fc_resultRelInfo->ri_WithCheckOptions = fc_wcoList;
		fc_resultRelInfo->ri_WithCheckOptionExprs = fc_wcoExprs;
		fc_resultRelInfo++;
	}

	/*
	 * 如果需要，初始化RETURNING投影。
	 */
	if (fc_node->returningLists)
	{
		TupleTableSlot *fc_slot;
		ExprContext *fc_econtext;

		/*
		 * 初始化结果元组槽并使用第一个
		 * RETURNING列表分配其行类型。我们假设其余部分看起来相同。
		 */
		fc_mtstate->ps.plan->targetlist = (List *) linitial(fc_node->returningLists);

		/* 为RETURNING投影的输出设置一个槽 */
		ExecInitResultTupleSlotTL(&fc_mtstate->ps, &TTSOpsVirtual);
		fc_slot = fc_mtstate->ps.ps_ResultTupleSlot;

		/* 也需要一个econtext */
		if (fc_mtstate->ps.ps_ExprContext == NULL)
			ExecAssignExprContext(fc_estate, &fc_mtstate->ps);
		fc_econtext = fc_mtstate->ps.ps_ExprContext;

		/*
		 * 为每个结果关系构建一个投影。
		 */
		fc_resultRelInfo = fc_mtstate->resultRelInfo;
		foreach(fc_l, fc_node->returningLists)
		{
			List	   *fc_rlist = (List *) lfirst(fc_l);

			fc_resultRelInfo->ri_returningList = fc_rlist;
			fc_resultRelInfo->ri_projectReturning =
				ExecBuildProjectionInfo(fc_rlist, fc_econtext, fc_slot, &fc_mtstate->ps,
										fc_resultRelInfo->ri_RelationDesc->rd_att);
			fc_resultRelInfo++;
		}
	}
	else
	{
		/*
		 * 我们仍然必须构建一个虚拟结果元组类型，因为 InitPlan
		 * 期望有一个（也许应该改变这一点？）。
		 */
		fc_mtstate->ps.plan->targetlist = NIL;
		ExecInitResultTypeTL(&fc_mtstate->ps);

		fc_mtstate->ps.ps_ExprContext = NULL;
	}

	/* 如果需要，为 ON CONFLICT 设置仲裁索引列表 */
	fc_resultRelInfo = fc_mtstate->resultRelInfo;
	if (fc_node->onConflictAction != ONCONFLICT_NONE)
	{
		/* insert 只能有一个关系，继承关系不会展开 */
		Assert(fc_nrels == 1);
		fc_resultRelInfo->ri_onConflictArbiterIndexes = fc_node->arbiterIndexes;
	}

	/*
	 * 如果需要，为 ON CONFLICT
	 * DO UPDATE 初始化目标列表、投影和约束。
	 */
	if (fc_node->onConflictAction == ONCONFLICT_UPDATE)
	{
		OnConflictSetState *fc_onconfl = makeNode(OnConflictSetState);
		ExprContext *fc_econtext;
		TupleDesc	fc_relationDesc;

		/* 如果已通过上面的 RETURNING 处理创建，则已经存在 */
		if (fc_mtstate->ps.ps_ExprContext == NULL)
			ExecAssignExprContext(fc_estate, &fc_mtstate->ps);

		fc_econtext = fc_mtstate->ps.ps_ExprContext;
		fc_relationDesc = fc_resultRelInfo->ri_RelationDesc->rd_att;

		/* 创建 DO UPDATE SET 操作的状态 */
		fc_resultRelInfo->ri_onConflict = fc_onconfl;

		/* 为现有元组初始化槽 */
		fc_onconfl->oc_Existing =
			table_slot_create(fc_resultRelInfo->ri_RelationDesc,
							  &fc_mtstate->ps.state->es_tupleTable);

		/*
		 * 为 UPDATE SET 投影创建元组槽。我们希望这里有一个表的类型槽，因为
		 * 槽将用于插入到表中，并用于 RETURNING 处理 - 这可能访问
		 * 系统属性。
		 */
		fc_onconfl->oc_ProjSlot =
			table_slot_create(fc_resultRelInfo->ri_RelationDesc,
							  &fc_mtstate->ps.state->es_tupleTable);

		/* 构建 UPDATE SET 投影状态 */
		fc_onconfl->oc_ProjInfo =
			ExecBuildUpdateProjection(fc_node->onConflictSet,
									  true,
									  fc_node->onConflictCols,
									  fc_relationDesc,
									  fc_econtext,
									  fc_onconfl->oc_ProjSlot,
									  &fc_mtstate->ps);

		/* 初始化状态以评估 WHERE 子句（如果有） */
		if (fc_node->onConflictWhere)
		{
			ExprState  *fc_qualexpr;

			fc_qualexpr = ExecInitQual((List *) fc_node->onConflictWhere,
									&fc_mtstate->ps);
			fc_onconfl->oc_WhereClause = fc_qualexpr;
		}
	}

	/*
	 * 如果我们在 UPDATE 或 DELETE 中有任何辅助关系，这些关系需要
	 * 像 SELECT FOR UPDATE 中的非锁定关系一样处理，即，EvalPlanQual
	 * 机制需要知道它们。这对于 MERGE 中的源关系也适用。定位相关的 ExecRowMarks。
	 */
	fc_arowmarks = NIL;
	foreach(fc_l, fc_node->rowMarks)
	{
		PlanRowMark *fc_rc = lfirst_node(PlanRowMark, fc_l);
		ExecRowMark *fc_erm;
		ExecAuxRowMark *fc_aerm;

		/* 忽略“父”行标记；在运行时它们是无关的 */
		if (fc_rc->isParent)
			continue;

		/* 查找 ExecRowMark 并构建 ExecAuxRowMark */
		fc_erm = ExecFindRowMark(fc_estate, fc_rc->rti, false);
		fc_aerm = ExecBuildAuxRowMark(fc_erm, fc_subplan->targetlist);
		fc_arowmarks = lappend(fc_arowmarks, fc_aerm);
	}

	/* 对于 MERGE 命令，初始化其状态 */
	if (fc_mtstate->operation == CMD_MERGE)
		ExecInitMerge(fc_mtstate, fc_estate);

	EvalPlanQualSetPlan(&fc_mtstate->mt_epqstate, fc_subplan, fc_arowmarks);

	/*
	 * 如果结果关系很多，使用哈希表加速查找。
	 * 如果不多，简单的线性搜索更快。
	 *
	 * 不清楚阈值在哪里，但先尝试 64。在调试构建中，使用小阈值
	 * 以便我们在两个代码路径上都获得一些测试覆盖。
	 */
#ifdef USE_ASSERT_CHECKING
#define MT_NRELS_HASH 4
#else
#define MT_NRELS_HASH 64
#endif
	if (fc_nrels >= MT_NRELS_HASH)
	{
		HASHCTL		fc_hash_ctl;

		fc_hash_ctl.keysize = sizeof(Oid);
		fc_hash_ctl.entrysize = sizeof(MTTargetRelLookup);
		fc_hash_ctl.hcxt = CurrentMemoryContext;
		fc_mtstate->mt_resultOidHash =
			hash_create("ModifyTable target hash",
						fc_nrels, &fc_hash_ctl,
						HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
		for (fc_i = 0; fc_i < fc_nrels; fc_i++)
		{
			Oid			fc_hashkey;
			MTTargetRelLookup *fc_mtlookup;
			bool		fc_found;

			fc_resultRelInfo = &fc_mtstate->resultRelInfo[fc_i];
			fc_hashkey = RelationGetRelid(fc_resultRelInfo->ri_RelationDesc);
			fc_mtlookup = (MTTargetRelLookup *)
				hash_search(fc_mtstate->mt_resultOidHash, &fc_hashkey,
							HASH_ENTER, &fc_found);
			Assert(!fc_found);
			fc_mtlookup->relationIndex = fc_i;
		}
	}
	else
		fc_mtstate->mt_resultOidHash = NULL;

	/*
	 * 确定 FDW 是否支持批量插入并确定批量大小
	 * （一个 FDW 可能支持批量处理，但可能对
	 * 服务器/表禁用）。
	 *
	 * 我们仅在 INSERT 时执行此操作，以便对 UPDATE/DELETE 批量大小
	 * 保持设置为 0。
	 */
	if (fc_operation == CMD_INSERT)
	{
		/* insert 只能有一个关系，继承关系不会展开 */
		Assert(fc_nrels == 1);
		fc_resultRelInfo = fc_mtstate->resultRelInfo;
		if (!fc_resultRelInfo->ri_usesFdwDirectModify &&
			fc_resultRelInfo->ri_FdwRoutine != NULL &&
			fc_resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize &&
			fc_resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert)
		{
			fc_resultRelInfo->ri_BatchSize =
				fc_resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize(fc_resultRelInfo);
			Assert(fc_resultRelInfo->ri_BatchSize >= 1);
		}
		else
			fc_resultRelInfo->ri_BatchSize = 1;
	}

	/*
	 * 最后，如果这不是主（canSetTag）ModifyTable 节点，将其
	 * 添加到 estate->es_auxmodifytables，以便它将在
	 * ExecPostprocessPlan 由 ExecPostprocessPlan 完成运行。
	 * （其实将主 ModifyTable 节点也添加进去是可以的，但没有必要。）
	 * 请注意使用 lcons 而非 lappend：我们需要稍后初始化的 ModifyTable 
	 * 节点在早期节点之前关闭。这确保了我们不会丢弃需要由
	 * 后面的 CTE 子计划看到的 RETURNING 行。
	 */
	if (!fc_mtstate->canSetTag)
		fc_estate->es_auxmodifytables = lcons(fc_mtstate,
										   fc_estate->es_auxmodifytables);

	return fc_mtstate;
}

/* ----------------------------------------------------------------
 *		ExecEndModifyTable
 *
 *		关闭计划。
 *
 *		不返回任何感兴趣的内容。
 * ----------------------------------------------------------------
 */
void ExecEndModifyTable(ModifyTableState *fc_node)
{
	int			fc_i;

	/*
	 * 允许任何 FDW 关闭
	 */
	for (fc_i = 0; fc_i < fc_node->mt_nrels; fc_i++)
	{
		int			fc_j;
		ResultRelInfo *fc_resultRelInfo = fc_node->resultRelInfo + fc_i;

		if (!fc_resultRelInfo->ri_usesFdwDirectModify &&
			fc_resultRelInfo->ri_FdwRoutine != NULL &&
			fc_resultRelInfo->ri_FdwRoutine->EndForeignModify != NULL)
			fc_resultRelInfo->ri_FdwRoutine->EndForeignModify(fc_node->ps.state,
														   fc_resultRelInfo);

		/*
		 * 清理初始化的批处理槽。这只对具有批处理的 FDW 有意义，
		 * 但其他情况下 ri_NumSlotsInitialized 将为 0。
		 */
		for (fc_j = 0; fc_j < fc_resultRelInfo->ri_NumSlotsInitialized; fc_j++)
		{
			ExecDropSingleTupleTableSlot(fc_resultRelInfo->ri_Slots[fc_j]);
			ExecDropSingleTupleTableSlot(fc_resultRelInfo->ri_PlanSlots[fc_j]);
		}
	}

	/*
	 * 关闭所有分区表、叶子分区及其索引，
	 * 并释放用于元组路由的槽（如果设置）。
	 */
	if (fc_node->mt_partition_tuple_routing)
	{
		ExecCleanupTupleRouting(fc_node, fc_node->mt_partition_tuple_routing);

		if (fc_node->mt_root_tuple_slot)
			ExecDropSingleTupleTableSlot(fc_node->mt_root_tuple_slot);
	}

	/*
	 * 释放表达式上下文
	 */
	ExecFreeExprContext(&fc_node->ps);

	/*
	 * 清空元组表
	 */
	if (fc_node->ps.ps_ResultTupleSlot)
		ExecClearTuple(fc_node->ps.ps_ResultTupleSlot);

	/*
	 * 如果活动，则终止 EPQ 执行
	 */
	EvalPlanQualEnd(&fc_node->mt_epqstate);

	/*
	 * 关闭子计划
	 */
	ExecEndNode(outerPlanState(fc_node));
}

void ExecReScanModifyTable(ModifyTableState *fc_node)
{
	/*
	 * 目前，我们不需要支持 ModifyTable 节点的重新扫描。其
	 * 语义本身会有些争议。
	 */
	elog(ERROR, "ExecReScanModifyTable is not implemented");
}
